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:
40
AGENTS.md
40
AGENTS.md
@@ -59,38 +59,37 @@ castle program add <path|git-url> [--name ...] # adopt EXISTIN
|
|||||||
castle program clone [name] # provision repo: source
|
castle program clone [name] # provision repo: source
|
||||||
castle program delete <name> [--source] [-y]
|
castle program delete <name> [--source] [-y]
|
||||||
castle program run <name> [args...] # declared run command
|
castle program run <name> [args...] # declared run command
|
||||||
castle program install|uninstall [name] # activate tools/statics
|
|
||||||
castle program build|test|lint|format|type-check|check [name] # dev verbs
|
castle program build|test|lint|format|type-check|check [name] # dev verbs
|
||||||
|
|
||||||
# Services — daemons (manager: systemd, no schedule)
|
# Services — daemons (manager: systemd, no schedule)
|
||||||
castle service list|info <name> [--json]
|
castle service list|info <name> [--json]
|
||||||
castle service create <name> [--program P] [--port N] [--health ...] [--launcher ...]
|
castle service create <name> [--program P] [--port N] [--health ...] [--launcher ...]
|
||||||
castle service deploy <name> # generate unit + gateway route
|
castle service restart <name> # imperative bounce
|
||||||
castle service enable|disable <name> # systemd enable/disable (+ boot)
|
|
||||||
castle service start|stop|restart <name>
|
|
||||||
castle service logs <name> [-f] [-n 50]
|
castle service logs <name> [-f] [-n 50]
|
||||||
|
|
||||||
# Jobs — scheduled tasks (manager: systemd + schedule). Same verbs; create takes --schedule
|
# Jobs — scheduled tasks (manager: systemd + schedule). create takes --schedule
|
||||||
castle job create <name> [--program P] --schedule "0 2 * * *" [--launcher ...]
|
castle job create <name> [--program P] --schedule "0 2 * * *" [--launcher ...]
|
||||||
castle job <list|info|delete|deploy|enable|disable|start|stop|restart|logs> ...
|
castle job <list|info|delete|restart|logs> ...
|
||||||
|
|
||||||
# Tools — CLIs on PATH (manager: path)
|
# Tools — CLIs on PATH (manager: path)
|
||||||
castle tool list [--json] # each tool's executable + description + install state
|
castle tool list [--json] # each tool's executable + description + install state
|
||||||
castle tool info <name> [--json]
|
castle tool info <name> [--json]
|
||||||
castle tool install|uninstall <name>
|
|
||||||
|
|
||||||
# Platform-wide
|
# Platform-wide
|
||||||
castle list [--kind ...] [--stack ...] [--json] # all deployments
|
castle apply [name] [--plan] # converge runtime to config — the workhorse
|
||||||
|
castle list [--kind ...] [--stack ...] [--json] # all deployments
|
||||||
castle status # unified health/status
|
castle status # unified health/status
|
||||||
castle doctor # diagnose setup + runtime, with fix hints
|
castle doctor # diagnose setup + runtime, with fix hints
|
||||||
castle deploy [name] # apply config → units + Caddyfile
|
castle restart [name] # imperative bounce (one or all)
|
||||||
castle start | stop | restart # all services (+ gateway)
|
castle gateway reload|status # the Caddy gateway
|
||||||
castle gateway start|stop|reload|status # the Caddy gateway
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`castle service`/`job`/`tool` are **views** over the one deployment set, filtered
|
`castle service`/`job`/`tool` are **views** over the one deployment set, filtered
|
||||||
by derived kind. Bringing everything online is the two honest steps
|
by derived kind. Lifecycle is **convergence**: edit config, then **`castle apply`**
|
||||||
**`castle deploy && castle start`** (apply config, then start).
|
renders units + the Caddyfile and reconciles the runtime (activate what's enabled,
|
||||||
|
restart what changed, deactivate what's disabled). To durably turn a deployment
|
||||||
|
off, set `enabled: false` and apply — there is no separate start/stop/enable.
|
||||||
|
`castle restart` is the one imperative bounce.
|
||||||
|
|
||||||
**Dev verbs resolve per-program:** a declared `commands:` entry (or `build:`)
|
**Dev verbs resolve per-program:** a declared `commands:` entry (or `build:`)
|
||||||
overrides the program's stack default, else the stack handler, else the verb is
|
overrides the program's stack default, else the stack handler, else the verb is
|
||||||
@@ -108,8 +107,7 @@ castle program create my-service --stack python-fastapi --description "Does X"
|
|||||||
cd /data/repos/my-service && uv sync # implement it
|
cd /data/repos/my-service && uv sync # implement it
|
||||||
castle program test my-service
|
castle program test my-service
|
||||||
castle service create my-service --program my-service --port 9001
|
castle service create my-service --program my-service --port 9001
|
||||||
castle service deploy my-service && castle service enable my-service
|
castle apply my-service # renders the unit + gateway route and starts it
|
||||||
castle gateway reload
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The service reads its port/data dir from env vars that `deployments/my-service.yaml`
|
The service reads its port/data dir from env vars that `deployments/my-service.yaml`
|
||||||
@@ -120,7 +118,7 @@ maps via placeholders (see §6). Stack guide: **`docs/stacks/python-fastapi.md`*
|
|||||||
```bash
|
```bash
|
||||||
castle program create my-tool --stack python-cli --description "Does Y"
|
castle program create my-tool --stack python-cli --description "Does Y"
|
||||||
cd /data/repos/my-tool && uv sync
|
cd /data/repos/my-tool && uv sync
|
||||||
castle tool install my-tool # uv tool install → on PATH
|
castle apply my-tool # installs the path deployment on PATH
|
||||||
```
|
```
|
||||||
|
|
||||||
`castle tool list --json` is the machine-readable tool catalog (each tool's real
|
`castle tool list --json` is the machine-readable tool catalog (each tool's real
|
||||||
@@ -134,7 +132,7 @@ A job is a `manager: systemd` deployment with a `schedule` (cron). Generates a
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
castle job create nightly --program my-tool --schedule "0 2 * * *" --launcher command
|
castle job create nightly --program my-tool --schedule "0 2 * * *" --launcher command
|
||||||
castle job deploy nightly && castle job enable nightly
|
castle apply nightly
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create a static frontend
|
### Create a static frontend
|
||||||
@@ -143,7 +141,7 @@ castle job deploy nightly && castle job enable nightly
|
|||||||
# scaffold a Vite/React app under /data/repos/my-frontend (see docs/stacks/react-vite.md)
|
# scaffold a Vite/React app under /data/repos/my-frontend (see docs/stacks/react-vite.md)
|
||||||
castle program build my-frontend # produces dist/
|
castle program build my-frontend # produces dist/
|
||||||
# deployments/my-frontend.yaml → manager: caddy, root: dist
|
# deployments/my-frontend.yaml → manager: caddy, root: dist
|
||||||
castle deploy && castle gateway reload # served at my-frontend.<domain>
|
castle apply # served at my-frontend.<domain>
|
||||||
```
|
```
|
||||||
|
|
||||||
The gateway serves the build **in place** from `<source>/<root>` — no copy, no
|
The gateway serves the build **in place** from `<source>/<root>` — no copy, no
|
||||||
@@ -213,7 +211,7 @@ context** (`crypto.subtle`, service workers), which plain-HTTP LAN hosts lack.
|
|||||||
`acme` operational prerequisites (castle can't do these for you):
|
`acme` operational prerequisites (castle can't do these for you):
|
||||||
- **DNS-plugin Caddy** at `/usr/local/bin/caddy` — `./install.sh --with-dns-plugin=cloudflare`.
|
- **DNS-plugin Caddy** at `/usr/local/bin/caddy` — `./install.sh --with-dns-plugin=cloudflare`.
|
||||||
- **Provider token** stored as a secret and mapped into the gateway service env
|
- **Provider token** stored as a secret and mapped into the gateway service env
|
||||||
(`CLOUDFLARE_API_TOKEN`); `castle deploy` warns if missing.
|
(`CLOUDFLARE_API_TOKEN`); `castle apply` warns if missing.
|
||||||
- **Bind :443/:80** — lower the floor once: `net.ipv4.ip_unprivileged_port_start=80`
|
- **Bind :443/:80** — lower the floor once: `net.ipv4.ip_unprivileged_port_start=80`
|
||||||
in `/etc/sysctl.d/` (beats `setcap`, which `NoNewPrivileges` would void).
|
in `/etc/sysctl.d/` (beats `setcap`, which `NoNewPrivileges` would void).
|
||||||
- **Stage first**: `CASTLE_ACME_STAGING=1` at deploy, verify issuance, then unset
|
- **Stage first**: `CASTLE_ACME_STAGING=1` at deploy, verify issuance, then unset
|
||||||
@@ -258,7 +256,7 @@ Roots: **`CASTLE_HOME`** (config/code/artifacts/secrets, default `~/.castle`) an
|
|||||||
|
|
||||||
`public: true` (requires `proxy: true`) projects a service to the internet at
|
`public: true` (requires `proxy: true`) projects a service to the internet at
|
||||||
`<name>.<gateway.public_domain>` (a **separate** zone, so internal subdomain names
|
`<name>.<gateway.public_domain>` (a **separate** zone, so internal subdomain names
|
||||||
stay out of public DNS). `castle deploy` generates the cloudflared ingress from the
|
stay out of public DNS). `castle apply` generates the cloudflared ingress from the
|
||||||
set of public services. Needs `gateway.public_domain` + `gateway.tunnel_id` set and
|
set of public services. Needs `gateway.public_domain` + `gateway.tunnel_id` set and
|
||||||
the `castle-tunnel` service running. → One-time setup: **`docs/tunnel-setup.md`**.
|
the `castle-tunnel` service running. → One-time setup: **`docs/tunnel-setup.md`**.
|
||||||
|
|
||||||
|
|||||||
37
README.md
37
README.md
@@ -55,9 +55,10 @@ A program can have several deployments — a CLI that is both a `tool` on PATH a
|
|||||||
scheduled `job` — so a program has no single kind of its own; it *has deployments*,
|
scheduled `job` — so a program has no single kind of its own; it *has deployments*,
|
||||||
each with its own.
|
each with its own.
|
||||||
|
|
||||||
Standing everything up is the two honest steps `castle deploy` (regenerate systemd
|
Standing everything up is one honest step: `castle apply` renders systemd units and
|
||||||
units and gateway config from your config) then `castle start` (enable/start it).
|
gateway config from your config, then reconciles the runtime to match — activating
|
||||||
There is no bundled "up".
|
what's enabled, restarting what changed, deactivating what's disabled. Edit config,
|
||||||
|
`castle apply`; `castle apply --plan` shows the diff first.
|
||||||
|
|
||||||
## Stacks
|
## Stacks
|
||||||
|
|
||||||
@@ -112,7 +113,7 @@ git clone <this-repo> ~/castle && cd ~/castle
|
|||||||
# Postgres), creates ~/.castle, registers Castle's own control plane, builds the UI.
|
# Postgres), creates ~/.castle, registers Castle's own control plane, builds the UI.
|
||||||
./install.sh
|
./install.sh
|
||||||
|
|
||||||
castle deploy && castle start # apply config to the runtime, then bring it up
|
castle apply # converge the runtime to config (units, routes, run)
|
||||||
castle doctor # verify — every check should be green
|
castle doctor # verify — every check should be green
|
||||||
open http://localhost:9000 # the dashboard
|
open http://localhost:9000 # the dashboard
|
||||||
```
|
```
|
||||||
@@ -124,7 +125,7 @@ looks off — after an install, a deploy, or a config change.
|
|||||||
### Exposure: from localhost to your own HTTPS domain
|
### Exposure: from localhost to your own HTTPS domain
|
||||||
|
|
||||||
Localhost is the first rung; you climb only as far as you need. Each rung is a small
|
Localhost is the first rung; you climb only as far as you need. Each rung is a small
|
||||||
config change plus `castle deploy`, and `castle doctor` tells you what a rung still
|
config change plus `castle apply`, and `castle doctor` tells you what a rung still
|
||||||
needs.
|
needs.
|
||||||
|
|
||||||
| Rung | You get | What it takes |
|
| Rung | You get | What it takes |
|
||||||
@@ -145,15 +146,15 @@ are still missing, each with its fix.
|
|||||||
# A service — FastAPI app + a systemd service deployment (health, unit, route)
|
# A service — FastAPI app + a systemd service deployment (health, unit, route)
|
||||||
castle program create my-api --stack python-fastapi --description "Does something"
|
castle program create my-api --stack python-fastapi --description "Does something"
|
||||||
castle program test my-api
|
castle program test my-api
|
||||||
castle deploy my-api && castle service enable my-api
|
castle apply my-api # render unit + route, then start it
|
||||||
|
|
||||||
# A tool — a CLI installed on your PATH
|
# A tool — a CLI installed on your PATH
|
||||||
castle program create my-tool --stack python-cli --description "Does something"
|
castle program create my-tool --stack python-cli --description "Does something"
|
||||||
castle tool install my-tool
|
castle apply my-tool # installs its path deployment on PATH
|
||||||
|
|
||||||
# A static frontend — built once, served by the gateway
|
# A static frontend — built once, served by the gateway
|
||||||
castle program create my-app --stack react-vite --description "Web interface"
|
castle program create my-app --stack react-vite --description "Web interface"
|
||||||
castle program build my-app && castle deploy
|
castle program build my-app && castle apply
|
||||||
|
|
||||||
# Adopt an existing repo (no stack needed — dev verbs detected or declared)
|
# Adopt an existing repo (no stack needed — dev verbs detected or declared)
|
||||||
castle program add ~/projects/some-rust-tool
|
castle program add ~/projects/some-rust-tool
|
||||||
@@ -162,28 +163,32 @@ castle program add ~/projects/some-rust-tool
|
|||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
Operations live under the resource they act on. `program` is the catalog;
|
Operations live under the resource they act on. `program` is the catalog;
|
||||||
`service`, `job`, and `tool` are **views** over the one deployment set; platform
|
`service`, `job`, and `tool` are **views** over the one deployment set. Lifecycle
|
||||||
lifecycle is top-level.
|
is convergence — `castle apply` — not a pile of per-kind verbs.
|
||||||
|
|
||||||
```
|
```
|
||||||
# Programs — the software catalog
|
# Programs — the software catalog
|
||||||
castle program list|info|create|add|clone|delete|run|install|uninstall
|
castle program list|info|create|add|clone|delete|run
|
||||||
castle program build|test|lint|type-check|check [name] # dev verbs
|
castle program build|test|lint|type-check|check [name] # dev verbs
|
||||||
|
|
||||||
# Deployment lenses (service = systemd, job = systemd + schedule, tool = path)
|
# Deployment lenses (service = systemd, job = systemd + schedule, tool = path)
|
||||||
castle service list|info|create|delete|deploy|enable|disable|start|stop|restart|logs
|
castle service list|info|create|delete|restart|logs
|
||||||
castle job …same verbs; create takes --schedule
|
castle job …same verbs; create takes --schedule
|
||||||
castle tool list|info|install|uninstall # CLIs on your PATH
|
castle tool list|info # CLIs on your PATH
|
||||||
|
|
||||||
# Platform-wide
|
# Platform-wide
|
||||||
|
castle apply [name] [--plan] # converge runtime to config — the workhorse
|
||||||
castle list [--kind K] [--stack S] [--json] # catalog + every deployment view
|
castle list [--kind K] [--stack S] [--json] # catalog + every deployment view
|
||||||
castle status # unified runtime status
|
castle status # unified runtime status
|
||||||
castle doctor # diagnose setup + health, with fix hints
|
castle doctor # diagnose setup + health, with fix hints
|
||||||
castle deploy [name] # apply config → units + Caddyfile
|
castle restart [name] # imperative bounce (one or all)
|
||||||
castle start | stop | restart # all deployments (+ gateway)
|
castle gateway reload|status
|
||||||
castle gateway start|stop|reload|status
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To turn a deployment off, set `enabled: false` in its config and `castle apply` —
|
||||||
|
there's no start/stop/enable/install verb; the manager decides the mechanism
|
||||||
|
(systemd unit, PATH install, gateway route), the verb is always `apply`.
|
||||||
|
|
||||||
`castle tool list --json` is the machine-readable tool catalog assistants use to
|
`castle tool list --json` is the machine-readable tool catalog assistants use to
|
||||||
build context — it surfaces each tool's actual **executable** (which can differ from
|
build context — it surfaces each tool's actual **executable** (which can differ from
|
||||||
the program name, e.g. `litellm-intent-router` installs `intent-router`), its
|
the program name, e.g. `litellm-intent-router` installs `intent-router`), its
|
||||||
|
|||||||
@@ -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:…}`
|
implicitly. Values may use `${port}`/`${data_dir}`/`${name}`/`${secret:…}`
|
||||||
placeholders, which deploy resolves into the registry's flat `env`.
|
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
|
```yaml
|
||||||
node:
|
node:
|
||||||
@@ -228,7 +228,7 @@ deployed:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The node config says what's deployed *here* and with what concrete
|
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,
|
`defaults.env` placeholders and secrets, resolves binary paths,
|
||||||
and writes the registry. Systemd units and Caddyfile are then generated
|
and writes the registry. Systemd units and Caddyfile are then generated
|
||||||
from the registry — never from the spec directly.
|
from the registry — never from the spec directly.
|
||||||
@@ -280,7 +280,7 @@ programs on a single node and across multiple Castle nodes.
|
|||||||
|
|
||||||
**MQTT topics:**
|
**MQTT topics:**
|
||||||
- `castle/{hostname}/registry` — retained JSON, full NodeRegistry.
|
- `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).
|
- `castle/{hostname}/status` — `"online"` (retained) / `"offline"` (LWT).
|
||||||
LWT ensures nodes are marked offline if they disconnect unexpectedly.
|
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)
|
├── code/ ← Program source (your programs)
|
||||||
│ └── <name>/
|
│ └── <name>/
|
||||||
├── artifacts/
|
├── artifacts/
|
||||||
│ ├── specs/ ← Generated by `castle deploy`
|
│ ├── specs/ ← Generated by `castle apply`
|
||||||
│ │ ├── Caddyfile
|
│ │ ├── Caddyfile
|
||||||
│ │ └── registry.yaml ← Node config (what's deployed here)
|
│ │ └── registry.yaml ← Node config (what's deployed here)
|
||||||
│ └── content/ ← Built frontend assets
|
│ └── content/ ← Built frontend assets
|
||||||
@@ -491,7 +491,7 @@ Castle's architecture parallels Erlang/OTP, mapped onto Unix:
|
|||||||
| Application | Component (independent, self-contained) |
|
| Application | Component (independent, self-contained) |
|
||||||
| Application resource file | Component spec in `castle.yaml` |
|
| Application resource file | Component spec in `castle.yaml` |
|
||||||
| Release config (sys.config) | Node config in `$CASTLE_HOME/artifacts/specs/registry.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) |
|
| Supervisor | systemd (restart policies, ordering) |
|
||||||
| Process | Running service/worker/job |
|
| Process | Running service/worker/job |
|
||||||
| Application env | Env vars |
|
| Application env | Env vars |
|
||||||
@@ -527,12 +527,12 @@ What exists today:
|
|||||||
- **CLI** — `castle` command, installed via `uv tool install --editable cli/`
|
- **CLI** — `castle` command, installed via `uv tool install --editable cli/`
|
||||||
- **Three packages** — `castle-core` (models, config, generators),
|
- **Three packages** — `castle-core` (models, config, generators),
|
||||||
`castle-cli` (commands), `castle-api` (HTTP API)
|
`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
|
`$CASTLE_HOME/artifacts/specs/registry.yaml` (node config). Systemd units and
|
||||||
Caddyfile generated from registry with fully resolved paths. No repo references
|
Caddyfile generated from registry with fully resolved paths. No repo references
|
||||||
in runtime artifacts.
|
in runtime artifacts.
|
||||||
- **Explicit env with placeholders** — a deployment's env is exactly its
|
- **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.
|
`${secret:…}` into concrete values. No hidden convention injection.
|
||||||
- **Gateway** — Caddy on port 9000, Caddyfile generated from registry
|
- **Gateway** — Caddy on port 9000, Caddyfile generated from registry
|
||||||
- **API** — `castle-api` on port 9020, reads from registry (optional
|
- **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`.
|
at `/usr/local/bin/caddy` when `tls: acme`.
|
||||||
- Systemd user units at `~/.config/systemd/user/castle-*.service` (+ `.timer`);
|
- 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`
|
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
|
- The `container` launcher resolves docker via `shutil.which("docker")` (preferred
|
||||||
over rootless podman on this box).
|
over rootless podman on this box).
|
||||||
- Service data at `$CASTLE_DATA_DIR/<name>/`; secrets at `~/.castle/secrets/`
|
- 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.
|
(§DNS). Verify: `dig +short <name>.<domain>` → the node's IP.
|
||||||
3. **Set `gateway.tls: acme`** (with `domain`/`acme_email`), plus the operational
|
3. **Set `gateway.tls: acme`** (with `domain`/`acme_email`), plus the operational
|
||||||
prerequisites (below).
|
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).
|
5. **Update the app's origin allowlist** if it has one (§secure context).
|
||||||
|
|
||||||
## Operational prerequisites
|
## Operational prerequisites
|
||||||
@@ -142,7 +142,7 @@ a DNS token.
|
|||||||
(`~/.castle/secrets/<TOKEN_NAME>`, scope: the DNS provider's "edit DNS records"
|
(`~/.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
|
permission for your zone) and map it into the gateway service env in
|
||||||
`deployments/castle-gateway.yaml` (`defaults.env`), so Caddy reads it as
|
`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.
|
missing.
|
||||||
- **`acme` — stage first.** Set `CASTLE_ACME_STAGING=1` at deploy to use Let's
|
- **`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
|
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
|
`public: true` additionally projects a proxied service to the public internet via a
|
||||||
Cloudflare tunnel, at **`<service-name>.<gateway.public_domain>`** (a separate zone,
|
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
|
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` +
|
ingress from the set of public services. Needs `gateway.public_domain` +
|
||||||
`gateway.tunnel_id` set and the `castle-tunnel` service running; see
|
`gateway.tunnel_id` set and the `castle-tunnel` service running; see
|
||||||
@docs/tunnel-setup.md for the one-time setup.
|
@docs/tunnel-setup.md for the one-time setup.
|
||||||
@@ -467,7 +467,7 @@ Setup (the parts castle can't do for you):
|
|||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${secret:CLOUDFLARE_API_TOKEN}
|
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)
|
- **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>`
|
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
|
(dnsmasq) or the equivalent A record. The public zone gets no A records, so
|
||||||
@@ -493,8 +493,9 @@ manage:
|
|||||||
systemd: {}
|
systemd: {}
|
||||||
```
|
```
|
||||||
|
|
||||||
Enables `castle service enable/disable` and `castle service logs`. An empty `{}`
|
Marks the deployment systemd-managed, so `castle apply` generates and reconciles
|
||||||
uses defaults (enable=true, restart=on-failure, restart_sec=2).
|
its unit (and `castle service logs` tails it). An empty `{}` uses defaults
|
||||||
|
(enable=true, restart=on-failure, restart_sec=2).
|
||||||
|
|
||||||
Full options:
|
Full options:
|
||||||
```yaml
|
```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
|
cd /data/repos/my-service && uv sync # 2. Install deps
|
||||||
# ... implement ...
|
# ... implement ...
|
||||||
castle program test my-service # 3. Run tests
|
castle program test my-service # 3. Run tests
|
||||||
castle service enable my-service # 4. Generate systemd unit, start
|
castle apply my-service # 4. Render unit + routes and reconcile (start it)
|
||||||
castle gateway reload # 5. Update Caddy routes
|
|
||||||
```
|
```
|
||||||
|
|
||||||
After `service enable`, the service starts automatically on boot and restarts
|
`castle apply` renders the systemd unit + Caddy route and starts the service
|
||||||
on failure. Manage with:
|
(which then runs on boot and restarts on failure). Manage with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
castle logs my-service -f # Tail logs
|
castle logs my-service -f # Tail logs
|
||||||
castle service run my-service # Run in foreground (for debugging)
|
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
|
### 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
|
cd /data/repos/my-tool && uv sync # 2. Install deps
|
||||||
# ... implement ...
|
# ... implement ...
|
||||||
castle program test my-tool # 3. Run tests
|
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
|
### Job lifecycle
|
||||||
@@ -676,7 +677,7 @@ manage:
|
|||||||
systemd: {}
|
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.
|
and a `.timer` file.
|
||||||
|
|
||||||
## Infrastructure paths
|
## Infrastructure paths
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ See @docs/registry.md for the full registry reference.
|
|||||||
## Serving with Caddy
|
## Serving with Caddy
|
||||||
|
|
||||||
For production, the static build output is served by Caddy rather than a Node
|
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:
|
The flow:
|
||||||
|
|
||||||
1. You build the frontend with `castle program build <name>` (deploy does **not**
|
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 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.
|
`supabase` service** for PostgREST to pick up the added/removed schema list.
|
||||||
|
|
||||||
## Auth, RLS & the three privacy layers
|
## 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 create my-app --stack supabase --description "..." # scaffold + register
|
||||||
castle program build my-app # apply unapplied migrations to the substrate
|
castle program build my-app # apply unapplied migrations to the substrate
|
||||||
castle program test my-app # deno test over functions/ (if deno present)
|
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
|
## 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
|
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
|
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
|
*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.
|
the ingress from the registry.
|
||||||
- **One kill switch.** Stop `castle-tunnel` → instantly nothing is public.
|
- **One kill switch.** Stop `castle-tunnel` → instantly nothing is public.
|
||||||
|
|
||||||
@@ -76,8 +76,8 @@ manage:
|
|||||||
Bring it online:
|
Bring it online:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
castle deploy # writes cloudflared.yml from public services
|
castle apply # writes cloudflared.yml from public services
|
||||||
castle service enable castle-tunnel # start the tunnel
|
castle apply castle-tunnel # start the tunnel
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using the toggle
|
## Using the toggle
|
||||||
@@ -92,10 +92,10 @@ public: true # also expose at <name>.pub.payne.io via the tunnel
|
|||||||
Then just deploy:
|
Then just deploy:
|
||||||
|
|
||||||
```bash
|
```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
|
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
|
`false` (or remove it) and redeploy to un-expose — the hostname drops out of the
|
||||||
ingress immediately.
|
ingress immediately.
|
||||||
@@ -116,14 +116,14 @@ printf %s "<token>" > ~/.castle/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN
|
|||||||
chmod 600 ~/.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
|
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
|
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.
|
(`<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
|
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.
|
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
|
```bash
|
||||||
cloudflared tunnel route dns <tunnel-id> <name>.pub.payne.io
|
cloudflared tunnel route dns <tunnel-id> <name>.pub.payne.io
|
||||||
|
|||||||
15
install.sh
15
install.sh
@@ -22,7 +22,7 @@ CASTLE_HOME="${HOME}/.castle"
|
|||||||
CASTLE_CONF="${CASTLE_HOME}/infra.conf"
|
CASTLE_CONF="${CASTLE_HOME}/infra.conf"
|
||||||
# Program data lives on a dedicated volume, decoupled from CASTLE_HOME — must match
|
# Program data lives on a dedicated volume, decoupled from CASTLE_HOME — must match
|
||||||
# castle_core.config._resolve_data_dir (default /data/castle, override CASTLE_DATA_DIR)
|
# castle_core.config._resolve_data_dir (default /data/castle, override CASTLE_DATA_DIR)
|
||||||
# so the data dirs + container mounts created here line up with what castle deploy
|
# so the data dirs + container mounts created here line up with what castle apply
|
||||||
# generates for the mqtt/postgres/neo4j deployments.
|
# generates for the mqtt/postgres/neo4j deployments.
|
||||||
DATA_DIR="${CASTLE_DATA_DIR:-/data/castle}"
|
DATA_DIR="${CASTLE_DATA_DIR:-/data/castle}"
|
||||||
# Program source repos (default /data/repos, override CASTLE_REPOS_DIR).
|
# Program source repos (default /data/repos, override CASTLE_REPOS_DIR).
|
||||||
@@ -177,7 +177,7 @@ CADDY_DNS_VERSION="${CADDY_DNS_VERSION:-v2.11.4}"
|
|||||||
# (Let's Encrypt wildcard via DNS-01). Stock apt Caddy has no DNS modules. The
|
# (Let's Encrypt wildcard via DNS-01). Stock apt Caddy has no DNS modules. The
|
||||||
# result goes to /usr/local/bin/caddy, which precedes /usr/bin on PATH, so the
|
# result goes to /usr/local/bin/caddy, which precedes /usr/bin on PATH, so the
|
||||||
# gateway (a `command` runner resolving `caddy` via PATH) picks it up on the next
|
# gateway (a `command` runner resolving `caddy` via PATH) picks it up on the next
|
||||||
# `castle deploy` with no spec change. Idempotent and opt-in (--with-dns-plugin).
|
# `castle apply` with no spec change. Idempotent and opt-in (--with-dns-plugin).
|
||||||
ensure_caddy_dns_plugin() {
|
ensure_caddy_dns_plugin() {
|
||||||
local provider="${1:-cloudflare}"
|
local provider="${1:-cloudflare}"
|
||||||
local module
|
local module
|
||||||
@@ -211,7 +211,7 @@ ensure_caddy_dns_plugin() {
|
|||||||
|
|
||||||
/usr/local/bin/caddy list-modules 2>/dev/null | grep -q "dns.providers.$provider" \
|
/usr/local/bin/caddy list-modules 2>/dev/null | grep -q "dns.providers.$provider" \
|
||||||
|| log_fail "built caddy is missing dns.providers.$provider"
|
|| log_fail "built caddy is missing dns.providers.$provider"
|
||||||
log_info "Built /usr/local/bin/caddy — run 'castle deploy && castle gateway restart' to use it."
|
log_info "Built /usr/local/bin/caddy — run 'castle apply' to use it."
|
||||||
log_ok
|
log_ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ create_directories() {
|
|||||||
|
|
||||||
# Program data volume (default /data/castle) lives outside $HOME, so on a fresh
|
# Program data volume (default /data/castle) lives outside $HOME, so on a fresh
|
||||||
# machine its parent may not be user-writable — fall back to sudo + chown so the
|
# machine its parent may not be user-writable — fall back to sudo + chown so the
|
||||||
# later container mounts (and castle deploy) can write there.
|
# later container mounts (and castle apply) can write there.
|
||||||
if ! mkdir -p "${DATA_DIR}" 2>/dev/null; then
|
if ! mkdir -p "${DATA_DIR}" 2>/dev/null; then
|
||||||
log_info "creating ${DATA_DIR} (needs sudo — outside \$HOME)"
|
log_info "creating ${DATA_DIR} (needs sudo — outside \$HOME)"
|
||||||
sudo mkdir -p "${DATA_DIR}"
|
sudo mkdir -p "${DATA_DIR}"
|
||||||
@@ -280,7 +280,7 @@ create_directories() {
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# Register Castle's own control-plane programs/deployments from bootstrap/ so a
|
# Register Castle's own control-plane programs/deployments from bootstrap/ so a
|
||||||
# fresh registry is not empty. Without this, `castle deploy && castle start`
|
# fresh registry is not empty. Without this, `castle apply`
|
||||||
# would bring up nothing. Never clobbers existing entries (idempotent). The
|
# would bring up nothing. Never clobbers existing entries (idempotent). The
|
||||||
# gateway deployment carries a `__SPECS_DIR__` placeholder (the source repo has
|
# gateway deployment carries a `__SPECS_DIR__` placeholder (the source repo has
|
||||||
# no machine-specific paths) that we substitute with this machine's specs dir.
|
# no machine-specific paths) that we substitute with this machine's specs dir.
|
||||||
@@ -337,7 +337,7 @@ seed_caddyfile() {
|
|||||||
fi
|
fi
|
||||||
cat > "$caddyfile" << 'EOF'
|
cat > "$caddyfile" << 'EOF'
|
||||||
:9000 {
|
:9000 {
|
||||||
respond "Castle is starting. Run 'castle deploy' to configure." 200
|
respond "Castle is starting. Run 'castle apply' to configure." 200
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
log_ok
|
log_ok
|
||||||
@@ -553,8 +553,7 @@ print_summary() {
|
|||||||
printf "\n"
|
printf "\n"
|
||||||
|
|
||||||
printf "Next steps:\n"
|
printf "Next steps:\n"
|
||||||
printf " castle deploy # Generate registry, systemd units, Caddyfile\n"
|
printf " castle apply # Converge: render units/routes + start everything\n"
|
||||||
printf " castle start # Start the gateway, API, and all deployments\n"
|
|
||||||
printf " castle doctor # Verify setup + health (green = good to go)\n"
|
printf " castle doctor # Verify setup + health (green = good to go)\n"
|
||||||
printf " open http://localhost:9000 # the dashboard\n"
|
printf " open http://localhost:9000 # the dashboard\n"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user