Deployment (wip-deploy)
wip-deploy
What it is. wip-deploy is the operator-facing CLI that builds, validates, renders, and applies a WIP deployment. It turns a named preset plus CLI flags into a typed Deployment spec, discovers every wip-component.yaml / wip-app.yaml manifest in a WIP repo checkout, validates the two against each other, renders target-specific config (compose file, Kubernetes manifests, or a dev-mode compose variant with source mounts), and applies it — podman-compose up -d / docker compose up -d for compose and dev targets, kubectl apply for k8s. It also carries the day-2 surface: status, rebuild/restart/redeploy, incremental add/remove of apps and optional modules, pause/resume, teardown, and a handful of pre-flight and diagnostic verbs (validate-manifest, check-app-deployability).
Install & invoke. wip-deploy is a Python console-script entry point — the package wip-deploy (version 2.0.0a0, requires Python ≥3.11) registers wip-deploy = wip_deploy.cli:main. Once installed (pip install from the deployer/ project), the binary is on PATH as wip-deploy. Top-level usage:
wip-deploy [OPTIONS] COMMAND [ARGS]...
Running wip-deploy with no arguments prints help (the Typer app is built with no_args_is_help=True). wip-deploy --version prints wip-deploy 2.0.0a0 and exits. The app's own help banner: "WIP declarative deployer (v2)."; its epilog points at wip-deploy examples for common workflows and wip-deploy COMMAND --help for command-specific options. Every subcommand supports --help.
Commands
For the four verbs that assemble a full Deployment from a preset (validate, show-spec, render, install), the flag set is identical — all four call the same internal _assemble() helper. That shared set is listed once here; each of those four commands' own section below notes only what's additional or different.
Shared assemble flags (validate · show-spec · render · install)
| Flag | Default | Controls |
|---|---|---|
--preset, -p | standard | Preset name: core | headless | standard | analytics | full (see Presets below). |
--target, -t | compose | Deployment target: compose | k8s | dev. |
--variant | dev | Security posture dev | prod. prod injects WIP_VARIANT=prod, arming services' guards that refuse to start on known-default secrets. Explicit operator intent — never derived from --target; persists in the install's saved state across redeploy/rebuild. |
--hostname | None → localhost if --target dev, else wip.local | External hostname as seen by browsers. |
--tls | internal | TLS mode: internal | letsencrypt | external | self-signed. internal is auto-upgraded to self-signed when --target k8s (a cert + Secret are generated pre-install). |
--https-port | None → 443 (k8s) / 8443 (compose, dev) | External HTTPS port. |
--http-port | None → 80 (k8s) / 8080 (compose, dev) | External HTTP port. |
--data-dir | None → <repo-root>/data | Compose data directory (compose target only). |
--namespace | wip | K8s namespace. Bidirectional with --name: --namespace X alone auto-derives --name X; --name X alone (target k8s, namespace still at the literal default wip) auto-derives --namespace X. Pass both explicitly to diverge. |
--storage-class | rook-ceph-block | K8s StorageClass for PVCs. |
--ingress-class | nginx | K8s IngressClass name. |
--tls-secret-name | wip-tls | K8s TLS Secret name. |
--dev-mode | simple | Dev mode: simple (compose + source mounts + --reload) | tilt (reserved, not implemented). |
--registry | None | Image registry (e.g. ghcr.io/you). |
--tag | None | Deployment-wide image tag. When set, authoritative over manifest pins for every WIP-built component/app image. Unset → manifest pins apply, then latest. Fully-qualified infra images (mongo, postgres, dex, …) keep their own pins regardless. |
--image-tag | [] (repeatable) | Per-service override, NAME=TAG. Highest precedence — beats both --tag and manifest pins. |
--add | [] (repeatable) | Add an optional module to modules.optional. |
--remove | [] (repeatable) | Remove an optional module. |
--app | [] (repeatable) | Enable an app by name. |
--app-source | [] (repeatable) | Dev-target only: NAME=PATH — mount a local checkout as the app's build context instead of pulling the registry image. If <PATH>/Dockerfile.dev exists it's preferred over Dockerfile. Implicitly enables the named app. Ignored for target != dev. |
--app-from-registry | [] (repeatable) | Dev-target only: explicitly opt a named app into the registry-image fallback in dev mode. Without this, an enabled app with no --app-source and no local build context fails the dev install loudly. Implicitly enables the named app. Ignored for target != dev. |
--remote-wip | None | URL of a remote WIP install this install's apps should talk to. Apps that declare WIP_BASE_URL via from_spec: network.external_base_url resolve to this URL instead of the local install's own public URL. |
--apps-only | False | Install ONLY the named apps + Caddy — no core services, no MongoDB, no router, no Dex, no auth-gateway. Implies --auth-mode api-key-only and disables the auth gateway. Requires at least one --app; rejects --add. |
--auth-mode | None | Override auth mode: oidc | api-key-only | hybrid. |
--auth-gateway / --no-auth-gateway | None | Override the auth gateway on/off. |
--secrets-backend | None → file | Secret backend. Only file is implemented for every target (including k8s — the k8s renderer bakes file-backend values into an in-cluster Secret). |
--secrets-location | None → ~/.wip-deploy/<name>/secrets | Secret backend location (for file/sops backends). |
--repo-root | None (env WIP_REPO_ROOT) | WIP repo root. Auto-detected by walking up from cwd for an ancestor holding both components/ and deployer/, falling back to the nearest .git. |
--name | None → the --namespace value for --target k8s, else default | Deployment name. Determines the install directory, ~/.wip-deploy/<name>/. |
Discovery errors (a manifest that fails YAML parsing or schema validation) abort with a listed error per bad manifest and exit 1, for every command that discovers (all four below, plus the mutation verbs).
Presets
| Preset | Optional modules enabled | Auth | Notes |
|---|---|---|---|
core | mcp-server | api-key-only, gateway off | Minimal API, no OIDC. Local dev, demos, trusted-network installs. Apps are opt-in via --app. |
headless | mcp-server | api-key-only, gateway off | Same shape as core — lowest footprint, no human-facing UI surface. Backend-for-another-system, MCP-only workflows, CI fixtures. |
standard | reporting-sync, minio, mcp-server | hybrid, gateway on | Recommended default. OIDC via Dex + auth-gateway; hybrid accepts both OIDC bearer JWTs (in-browser Console flows via the gateway) and raw X-API-Key headers (off-host / server-side app calls). Everything except ingest-gateway. |
analytics | reporting-sync, mcp-server | hybrid, gateway on | standard's reporting pipeline (Postgres + reporting-sync) without file storage — --add minio to add it. |
full | reporting-sync, ingest-gateway, minio, mcp-server | hybrid, gateway on | Everything: OIDC, gateway, reporting, files, ingest. Biggest footprint (Postgres + MinIO + NATS all active). |
--add NAME / --remove NAME tweak any preset's optional-module list after the fact; --app NAME (repeatable) is layered on top of every preset — no preset enables an app by default.
wip-deploy validate
- Purpose. Validate a deployment configuration without rendering or applying.
- Arguments & flags. The shared assemble flags above; no additional flags.
- What it does. Assembles the
Deployment(build + discover), prints a summary block (Preset:,Target:,Hostname:,Auth:mode/gateway,Components:count + active names,Apps:count + enabled names), then runs the cross-cutting validator across the deployment, discovered components, and discovered apps. Exits 0 with✓ Deployment valid.on success. On failure, prints✗ Validation failed (N error(s)):to stderr with each error listed, and exits 1. Never renders or applies anything. - Example.
wip-deploy validate --hostname wip.example.com --tls letsencrypt
wip-deploy show-spec
- Purpose. Build the
Deploymentfrom preset + flags and dump it — "what does--preset standardactually resolve to?" - Arguments & flags. The shared assemble flags above, plus
--format(yaml|json, defaultyaml). - What it does. Assembles the
Deploymentwith discovery skipped (skip_discovery=True— no manifest walk, no validation) and dumps the model as YAML or JSON to stdout. - Example.
wip-deploy show-spec --preset full --format json | jq '.spec.apps'
wip-deploy render
- Purpose. Render the deployment to a directory without applying — inspect the generated
docker-compose.yaml, Caddyfile, Dex config, or k8s manifests before starting the stack. - Arguments & flags. The shared assemble flags above, plus
--output-dir(default:~/.wip-deploy/<name>/). - What it does. Assembles + validates the
Deployment, resolves secrets (generating any that don't yet exist in the backend), dispatches to the target's renderer (compose, k8s, or dev-simple), and writes the resultingFileTreeto the output directory. If the target directory already has an imported external CA (secrets/external-ca.crt), the rendered tree mounts it automatically, mirroring whatinstallwould do. Prints✓ Rendered to <dir>followed by the list of written paths. Render-time errors (e.g. a dev app with no source and not opted into the registry fallback) print✗ render aborted: <message>and exit 2 instead of a traceback. - Example.
wip-deploy render --preset standard --output-dir /tmp/wip-render
wip-deploy install
- Purpose. Build → validate → render → apply. The end-to-end install verb.
- Arguments & flags. The shared assemble flags above, plus:
--install-dir— materialization directory (default~/.wip-deploy/<name>/).--no-wait— skip waiting for healthy.--wait-timeout— overridespec.apply.timeout_seconds(default 300 seconds;on_timeoutbehavior defaults tofail, i.e. a timed-out wait aborts the install).--reconcile— allow this install to drop apps or optional modules that were present in the previous install but are absent from the current invocation.
- What it does. Rejects any
--targetother thancompose/k8s/devwith exit 2. Assembles + validates theDeployment. Applies--no-wait/--wait-timeouton top ofspec.apply. Compares against a previously-persisted install spec (if any): if apps or optional modules would be silently dropped, aborts with an itemized list unless--reconcileis passed. Runs preflight checks (port-free + stale-container warnings) for compose/dev targets only — k8s installs happen inside the cluster, no host ports to check. Resolves secrets (generating missing ones), renders the tree, and applies it —podman-compose up -d(ordocker compose) for compose/dev,kubectl applyfor k8s. Persists the appliedDeploymentto<install-dir>/deployment.deployer-state(a versioned JSON envelope) so later verbs (status --diff, the mutation verbs) can reuse the same spec. If the apply fails after it has already begun mutating the running deployment, the new spec is persisted anyway so subsequent commands reason from reality rather than stale state. Prints the install directory, an active-services/apps count, then✓ Install complete.(or a yellow "not all services reached healthy" warning if the wait timed out underon_timeout: warn|continue), followed by the reachable URL. - Example.
wip-deploy install --target dev --preset standard wip-deploy install --hostname wip.example.com --tls letsencrypt wip-deploy install --target k8s --namespace wip --tls external
wip-deploy status
- Purpose. Print a compact table of deployed services and their health.
- Arguments & flags.
--install-dir— compose/dev install directory (default from--name).--name— deployment name.--namespace,-n— query k8s instead of compose; auto-detected from saved state when reached via--namealone.--diff— k8s only: re-render manifests from the persisted spec and runkubectl diffagainst the live cluster; exits 0 (no drift), 1 (drift), or 2 (error).
- What it does. Without
--diff: for compose/dev, runspodman-compose ps(ordocker compose ps) in the install directory and prints aNAME / STATE / HEALTHtable, split into a "Services:" and an "Apps:" section when a persisted install spec is available (apps declared but not currently running are called out with a warning line). For k8s, runskubectl get pods -n <namespace>and prints the same table shape, using each pod'sReadycondition as the health column. With--diff: reloads the persistedDeployment, re-discovers manifests, re-resolves secrets, re-renders the tree into a temp directory, and shells out tokubectl diff -f <tmpdir> -R [-n <namespace>], propagating its exit code. - Example.
wip-deploy status --namespace wip-kb --diff
wip-deploy rebuild
- Purpose. Rebuild and recreate one or more services in an existing compose/dev install.
- Arguments & flags.
services(one or more positional names, required — matches keys underservices:in the rendereddocker-compose.yaml);--install-dir;--name;--no-wait;--wait-timeout(default 120 seconds). - What it does. Reads the already-rendered
docker-compose.yamldirectly — no spec load, no re-render; the install must already exist. Runscompose up -d --build --force-recreate <services...>. If waiting, pollscompose ps(with a fallback topodman inspect/docker inspectfor olderpodman-composeversions that omit aHealthfield) until every named service that declares a healthcheck reports healthy, or the timeout elapses (raises an error naming the still-unhealthy services rather than raising on the first check). - Example.
wip-deploy rebuild registry def-store --name wip-dev-local
wip-deploy restart
- Purpose. Restart one or more services in an existing install without rebuilding.
- Arguments & flags.
services(one or more positional names, required);--install-dir;--name. - What it does. Reads the rendered
docker-compose.yamland runscompose restart <services...>— stops and starts the existing containers in place; does not rebuild the image or recreate the container. Picks up environment-variable changes that don't propagate through bind-mounted source; does not pick up Dockerfile orrequirements.txtedits (userebuildfor those). - Example.
wip-deploy restart def-store template-store --name wip-dev-local
wip-deploy up
- Purpose. Bring an existing install back to running with no spec recomputation.
- Arguments & flags.
--install-dir;--name;--no-wait;--wait-timeout(default 120 seconds). - What it does. Compose/dev only. Reads the on-disk
docker-compose.yamlas-is (no build inputs, no re-render, no risk of dropping apps that weren't re-specified on the command line) and runscompose up -d. Picks up exited containers after a host reboot, laptop sleep, or a Podman-machine restart. Waits for every service in the compose file that declares a healthcheck, unless--no-wait. - Example.
wip-deploy up --name wip-dev-local --no-wait
wip-deploy stop
- Purpose. Halt a running install without deleting anything.
- Arguments & flags.
--install-dir;--name;--namespace,-n(k8s; auto-detected from saved state when reached via--namealone). - What it does. Run-state only — no render, no rebuild, no spec recompute. Compose/dev: runs
compose stop(stops the containers but keeps them — notcompose down). K8s: scales every WIP-labeled Deployment and StatefulSet (selectorapp.kubernetes.io/part-of=wip) to 0 replicas in one call — both workload kinds are scaled deliberately, so storage-bearing StatefulSets (MongoDB, Postgres, MinIO, NATS, Dex) are halted along with the stateless tier, not left running. PVCs, Services, Ingress, and secrets are left intact either way. Reverse withstart. - Example.
wip-deploy stop --namespace wip-kb
wip-deploy start
- Purpose. Bring a stopped install back to running — the reciprocal of
stop. - Arguments & flags.
--install-dir;--name;--namespace,-n. - What it does. Compose/dev: runs
compose start, reviving the stopped containers in place. K8s: scales every WIP-labeled Deployment and StatefulSet back to 1 replica (the renderer's hardcoded replica count). No render, no rebuild, no spec recompute. For post-reboot recovery of exited compose containers useupinstead —startis specifically the resume half ofstop. - Example.
wip-deploy start --name wip-dev-local
wip-deploy register-app
- Purpose. Register a local source checkout for an app, once, so future dev installs don't need
--app-source NAME=PATHre-typed every time. - Arguments & flags.
app_name(positional, required);--path(required — absolute path to the app's source checkout; must be a directory). - What it does. Writes
~/.wip-deploy/apps/<name>.yaml(name,local_path).wip-deploy install --target devconsults this per-operator, per-machine registry for every enabled app that has no--app-sourceoverride on the invocation itself; a per-invocation--app-sourcestill wins if both are set (with a warning printed to stderr when they disagree). Idempotent — re-running with the same name overwrites the entry, which is how a moved checkout is migrated. Operators can also hand-edit the YAML. - Example.
wip-deploy register-app react-console --path /Users/peter/Development/WIP-ReactConsole
wip-deploy unregister-app
- Purpose. Remove an app's local-source registration.
- Arguments & flags.
app_name(positional, required). - What it does. Deletes
~/.wip-deploy/apps/<name>.yaml. Silent and idempotent when the entry doesn't exist. - Example.
wip-deploy unregister-app kb
wip-deploy validate-manifest
- Purpose. Validate a standalone
wip-app.yamlagainst the current WIP root's discovered components/apps, without staging the manifest into the WIP repo first. - Arguments & flags.
manifest_path(positional, required — awip-app.yamlfile or a directory containing one);--repo-root(default: auto-discovered). - What it does. Two layers: (1) schema — Pydantic validation on the
Appmodel, catching missing fields, wrong types, bad enum values; (2) references — names used infrom_component*,depends_on, and route paths are checked against the WIP root's discovered components and apps, with any unresolved reference flagged alongside the list of valid alternatives. Out of scope: build context, env value resolution (from_secretaccepts any non-empty name), live cluster state. Exit 0 (valid), 1 (validation errors, printed to stderr), or 2 (manifest couldn't even be loaded — missing file, YAML parse error, not a mapping). - Example.
wip-deploy validate-manifest ./apps/react-console/wip-app.yaml --repo-root .
wip-deploy add-app
- Purpose. Add an app to an existing install without dropping anything else.
- Arguments & flags.
app_name(positional, required);--app-source/--source(dev target only — local checkout path for hot reload);--name;--install-dir;--repo-root. - What it does. Loads the persisted install spec (errors if none exists — mutation verbs only make sense against an existing install), re-discovers manifests, verifies the named app has a manifest, appends (or re-enables) it in
spec.apps, sets its devapp_sourcesentry if--sourcewas given, then validates, resolves secrets, re-renders, and applies — scoped to just the new app's compose service on compose/dev (k8s applies are already incremental, so no scoping concept applies there). Already-enabled with no source change is a no-op (⊙ ... no change). Persists the mutated spec. - Example.
wip-deploy add-app kb --app-source /Users/peter/Development/WIP-KB
wip-deploy remove-app
- Purpose. Remove an app from an existing install without dropping the rest.
- Arguments & flags.
app_name(positional, required);--name;--install-dir;--repo-root. - What it does. Drops the app's
AppReffromspec.appsand itsapp_sourcesentry (if any); a no-op with⊙ ... no changeif the app wasn't present. Force-removes the app's container directly (podman rm -f wip-<name>, ahead of the re-apply) so it doesn't linger as an orphan once the rendered compose stops mentioning it. Then validates, re-renders, and applies with an empty compose scope — nothing is brought back up for this app, but any proxy whose routing config changed (the dropped route) is gracefully reloaded. - Example.
wip-deploy remove-app clintrial --name wip-dev-local
wip-deploy app-deploy
- Purpose. Roll ONE already-enabled app to a new image tag on an existing install.
- Arguments & flags.
app_name(positional, required — must already be enabled);--tag(required, non-empty);--name;--install-dir;--repo-root. - What it does. Does NOT build or push — the tag must already exist on the install's registry (an app's own CI is expected to produce it). Sets
spec.images.tag_overrides[<app>](the install's own state, highest precedence over--tagand manifest pins; no WIP-repo manifest edit involved), then validates, re-renders, and applies scoped to just that app's container. A repeat call with the same tag is a no-op. - Example.
wip-deploy app-deploy wip-kb --tag sha-6ceff9a --name wip-local
wip-deploy redeploy
- Purpose. Re-render an existing install from its saved spec, recreating only what changed — the middle ground between
rebuild(recreate, no re-render) andinstall(full re-render from CLI flags, whole-stack blast radius). - Arguments & flags.
services(optional positional list — scope the apply to named compose services; rejected on k8s, wherekubectl applyis already incremental);--name;--install-dir;--repo-root. - What it does. Loads the persisted spec unchanged, re-discovers manifests, re-renders (so a deployer renderer or spec change since the last install takes effect), then re-applies through the same validate → resolve-secrets → apply → persist lifecycle as
install— but only services whose rendered config actually changed get recreated; everything else keeps running. If a service subset is given, it's validated against the currently-rendered compose service set before applying, so a typo fails cleanly. - Example.
wip-deploy redeploy --name wip-local wip-deploy redeploy registry def-store --name wip-local
wip-deploy add-module
- Purpose. Enable an optional module in an existing install without dropping anything.
- Arguments & flags.
module_name(positional, required — must match a discovered component withcategory: optional);--name;--install-dir;--repo-root. - What it does. Equivalent to passing
--add <module>on a fresh install, applied to the persisted spec instead. Errors if the module isn't a discovered optional component. No-op if already enabled. Appends tospec.modules.optional, then validates, re-renders, and applies — module verbs deliberately do a full re-up rather than a scoped one, because flipping a module can change other services' rendered environment (e.g. a dependent service picking up a newfrom_componentvalue). - Example.
wip-deploy add-module reporting-sync --name wip-dev-local
wip-deploy remove-module
- Purpose. Disable an optional module in an existing install without dropping anything else.
- Arguments & flags.
module_name(positional, required);--name;--install-dir;--repo-root. - What it does. Inverse of
add-module. No-op if not enabled. Force-removes the module's container (podman rm -f wip-<name>) ahead of the re-apply so it doesn't linger as an orphan, then validates, re-renders, and applies (full re-up, same reasoning asadd-module). - Example.
wip-deploy remove-module minio --name wip-dev-local
wip-deploy nuke
- Purpose. Tear down a WIP install.
- Arguments & flags.
--install-dir/--name— which install to tear down.--remove-data— also remove named volumes (compose) / cluster-scoped PVs bound to the namespace (k8s). Destructive.--remove-secrets— also remove the secret backend directory (or, host-wide, every install's secrets dir under--purge-all).--remove-images— also remove every image referenced by the compose file'sservices.*.imageentries (no-op for k8s — images live in the registry, not the operator's host).--secrets-location— override the secrets directory to remove.--purge-all— nuclear: everywip-*container/pod/network (and, with the flags above, volume/image/secrets dir) on the host, across every compose project. Refuses to run if any k8s install is found under~/.wip-deploy/on the host — k8s teardown must go throughnuke --name <name>orkubectl delete namespacedirectly.--dry-run— show what would be removed without removing it.-y/--yes— skip the confirmation prompt.
- What it does. Compose/dev (default path, without
--purge-all): runscompose downin the install directory (-vif--remove-data), then sweeps any danglingwip-*networks left behind (external: truenetworks thatcompose downdoesn't remove). Data (named volumes) and secrets both survive unless explicitly asked to go. K8s (default path, without--purge-all, auto-detected from the persisted install state): runskubectl delete namespace <ns> --ignore-not-found=true --wait=true— an unconditional cascading delete of every namespaced resource, including PVCs, regardless of--remove-data.--remove-dataadditionally hunts down and deletes any cluster-scoped PersistentVolumes that were bound to those PVCs (relevant when the storage class's reclaim policy isRetain, in which case the PV would otherwise survive in aReleasedstate holding the data).--purge-all: scans the whole host for anything matching WIP naming conventions (containers, pods, networks, and — opt-in — volumes/images/secrets dirs across every install under~/.wip-deploy/) and removes it, independent of any single compose project. - Example.
wip-deploy nuke --remove-data --yes wip-deploy nuke --purge-all --remove-data --remove-secrets --yes wip-deploy nuke --purge-all --dry-run
wip-deploy check-app-deployability
- Purpose. Check whether an app's source repo and manifest satisfy the deployable-app contract, before running
add-appagainst it. - Arguments & flags.
source_dir(positional, required — the app's source directory);--manifest(auto-detected by reading the source dir'spackage.jsonnamefield and matching againstapps/*/wip-app.yamlif omitted);--repo-root(default: auto-discovered). - What it does. Runs six checks and prints a ✓/✗ line per check with a fix hint on failure: (1)
Dockerfile.devis present — its absence makes the dev renderer fall back to the productionDockerfile, forcesNODE_ENV=development, and guards off the SPA static-asset block, producing a 404 at the app's URL; (2)vite.config.tssetshost: '0.0.0.0'— otherwise Vite binds only to localhost inside the container and the proxy hits a connection-refused; (3) the Vite dev-server proxy's target port matches the manifest's declared port — a common copy-paste mistake that leaves API calls returning 500 while the SPA still loads; (4)package.jsondeclares adevscript —Dockerfile.dev'sCMDtypically runsnpm run dev; (5) a manifest exists atapps/<name>/wip-app.yamland passes the same validation asvalidate-manifest; (6) the manifest declares both anhttpand adevport — without adevport, Caddy routes to the app's server process rather than Vite, breaking hot-reload and SPA serving. Not checked: a README development section (advisory only) and a live deployment test (that's whatadd-appitself is for). Exit 0 (all passed), 1 (at least one failed), or 2 (couldn't even start — source dir missing). - Example.
wip-deploy check-app-deployability ~/Development/WIP-KB
wip-deploy export-ca
- Purpose. Export the Caddy-managed internal CA root certificate so off-host clients (
curl, Node.js, embedded devices) can trust it. - Arguments & flags.
--name;--out(write to a file; omitted → PEM printed to stdout for piping). - What it does. Reads
/data/caddy/pki/authorities/local/root.crtlive out of the runningwip-caddycontainer viapodman exec ... cat, not from any host-side path — the install must currently be running. Only supported for installs using--tls internal(the compose/dev default);letsencryptinstalls need no CA export (browsers already trust the Let's Encrypt root) andexternal-terminated TLS is the upstream terminator's CA to hand out — both print an explanatory error and exit 2 instead of attempting an export. Not supported at all for k8s installs (which use an operator-provided cert in a TLS Secret, not a Caddy-generated one) — the error names thekubectl get secret ... -o jsonpathcommand to extract that instead. If Caddy hasn't yet lazily generated its CA (no HTTPS request has hit it), the error suggests acurl -kIprobe to trigger generation before retrying. On success with--out, prints per-OS trust instructions for Node.js, macOS, Linux, and curl. - Example.
wip-deploy export-ca --out wip-pi.local.crt
wip-deploy import-bundle
- Purpose. Seed an apps-only install from a bootstrap bundle — a single YAML file generated on the cloud/remote side carrying everything a cross-host apps-only install needs to connect: the external base URL, a scoped time-limited API key, and the remote's internal CA cert.
- Arguments & flags.
bundle_path(positional, required — a file path, or-to read from stdin);--name;--update-ca-only(refresh just the imported CA cert and exit, without touching the API key or other state — for when the remote rotates its CA). - What it does. Parses and validates the bundle: rejects an unrecognized
api_version/kind, rejects an already-expired API key, requiresscope.permissionsto be explicitly present and one ofread/write(no implicit-write default), and sniffs the CA cert for PEMBEGIN/END CERTIFICATEmarkers. On success, writessecrets/external-ca.crt(and, unless--update-ca-only,secrets/api-key) into the target install directory, bothchmod 0600, plus abootstrap.yamlaudit trail recording where the state came from. Does not run the install itself — it only seeds state; it prints a suggestedwip-deploy install --target dev --apps-only --remote-wip <url> ...command for the operator to run. Bundles ship unsigned in this version — signature verification is out of scope; the operator is trusted to have received the bundle from a legitimate source. - Example.
wip-deploy import-bundle laptop-rc.yaml --name laptop-rc ssh cloud.example "cat laptop-rc.yaml" | wip-deploy import-bundle - --name laptop-rc
wip-deploy examples
- Purpose. Print a curated map of common
wip-deployworkflows with their exact commands, grouped by intent (getting started, dev loop, changing an existing install, inspection, Kubernetes, pause/resume, teardown, presets, targets). - Arguments & flags. None.
- What it does. Prints a static, code-embedded text block — no computation, no repo access.
- Example.
wip-deploy examples
Workflows
dev
Local iteration against source, hot-reloading Python services via bind mounts.
wip-deploy install --target dev --preset standard— first install. The dev renderer materializes each component's build context into the rendered tree (patching inwip-auth, and fordocument-storealso the WIP-Toolkit dependency, the way the production build script does), bind-mountscomponents/<name>/srcread-only into the container, and appends--reloadto Python services' uvicorn commands. Node/Go apps get a build context but no hot reload. Every install withtarget=devforcescompose up -d --build --force-recreate, and — unless the apply is scoped to a subset of services — first force-removes any straywip-*containers from a prior install with a different compose project name, so a name collision can't leave a stale container mixed in with the fresh ones.wip-deploy install --target dev --app-source react-console=$HOME/Dev/WIP-ReactConsole— bind-mount an app's local checkout for hot-reload instead of pulling its registry image; repeatable for multiple apps.register-applets an operator register a checkout's path once instead of retyping--app-sourceon every install.- Iterate: edit component source → Python services pick it up live via
--reload; for aDockerfile/requirements.txt/package.jsonedit,wip-deploy rebuild <service>; for an env-var-only change,wip-deploy restart <service>(no rebuild). - Add or remove an app/module without touching the rest of the install:
wip-deploy add-app <name>/remove-app <name>/add-module <name>/remove-module <name>. wip-deploy statusto see what's running and healthy.- Cross-host apps-only variant: on the remote/cloud side, produce a bootstrap bundle; on the laptop side,
wip-deploy import-bundle <bundle> --name <name>seeds the CA + API key, then run the printedwip-deploy install --target dev --apps-only --remote-wip <url> --app <name>...— no local core services, apps talk to the remote WIP directly. - Post-reboot / Podman-machine restart:
wip-deploy up— brings back exited containers with no rebuild and no re-render. - Pause/resume without deleting anything:
wip-deploy stop/wip-deploy start. - Teardown:
wip-deploy nuke(add--remove-datato also drop volumes).
compose
Production-style single-host deployment via podman-compose / docker compose.
wip-deploy install --hostname wip-pi.local --preset standard(self-signed TLS by default) orwip-deploy install --hostname wip.example.com --tls letsencryptfor a public hostname.installassembles the spec, discovers manifests, runs preflight (port-free + stale-container checks), resolves/generates secrets, rendersdocker-compose.yaml+.env+config/caddy/Caddyfile(when the auth gateway or OIDC is active) +config/dex/config.yaml(when Dex is active), thencompose up -dand waits for every service with a healthcheck to report healthy (default 300-second budget; on timeout the install fails unlessspec.apply.on_timeoutsays otherwise). Post-install hooks run viacompose execonce health is confirmed.- The applied spec is persisted to
deployment.deployer-statein the install directory, so later commands don't need the original flags repeated. - Day-2 operations:
wip-deploy statusfor a health table;wip-deploy rebuild <svc>after an image change;wip-deploy restart <svc>for an env-only change;wip-deploy redeployto pick up a deployer renderer/spec change and recreate only what actually changed;wip-deploy add-app/remove-app/add-module/remove-modulefor incremental spec changes that leave the rest of the install untouched;wip-deploy app-deploy <app> --tag <tag>to roll a single app to a new pre-built image. wip-deploy export-ca --out <file>(for--tls internalinstalls) so off-host clients can trust the Caddy-generated CA.- Pause/resume:
wip-deploy stop/wip-deploy start. Post-reboot recovery of exited containers:wip-deploy up. - Teardown:
wip-deploy nuke(data and secrets preserved by default — a re-install reuses both) or, for cross-install host cleanup,wip-deploy nuke --purge-all.
k8s
Kubernetes manifests applied via kubectl.
wip-deploy render --target k8s --namespace wip --preset standardto inspect the generated manifests first (optional but recommended before a first apply).wip-deploy install --target k8s --namespace wip --tls external— requireskubectlalready configured against the target cluster.--tls internal(the flag's own default) is silently upgraded toself-signedfor this target; the install pre-flight then idempotently ensures akubernetes.io/tlsSecret exists, generating an RSA-4096 self-signed cert viaopensslon first run. If a same-named Secret already exists, its certificate's SAN is verified to actually cover the requested hostname before trusting it — a mismatch aborts with instructions to delete and re-mint, rather than serving a cert that will fail silently in the browser later.- Apply cleans any previously rendered top-level
*.yamland theservices//infrastructure/subdirectories under the install directory (without touching asecrets/backend directory that may share the path), writes the fresh tree, appliesnamespace.yamlfirst without pruning (Namespace is cluster-scoped — pruning it could touch a peer WIP namespace), then applies the rest of the tree withkubectl apply -R -f . --prune --selector=app.kubernetes.io/part-of=wipscoped to an explicit allow-list of kinds (Secrets, ConfigMaps, Services, PVCs, Deployments, StatefulSets, Ingresses, NetworkPolicies) — resources from a previous install that the current render no longer produces are pruned away. Waits viakubectl rollout statusper workload (a StatefulSet for anything with declared storage, a Deployment otherwise) up to the sharedspec.apply.timeout_secondsbudget, then runs post-install hooks viakubectl execagainst aRunningpod selected by the component's name label. - Day-2:
wip-deploy status --namespace <ns>(or--name <install>if the target/namespace were auto-detected from the saved state) for akubectl get pods-derived table;wip-deploy status --namespace <ns> --diffre-renders from the persisted spec and runskubectl diffagainst the live cluster. Mutation verbs (add-app,remove-app,add-module,remove-module,redeploy) work the same as compose, except any compose-style service-name scoping simply doesn't apply —kubectl applyis already incremental. - Pause/resume:
wip-deploy stop --namespace <ns>/wip-deploy start --namespace <ns>scale every WIP-labeled Deployment and StatefulSet to 0 / 1 replicas — PVCs, Services, Ingress, and secrets are untouched either way. - Teardown:
wip-deploy nuke --name <name>deletes the whole namespace (kubectl delete namespace, cascading to every namespaced resource including PVCs) unconditionally — this happens even without--remove-data.--remove-dataadditionally deletes any cluster-scoped PersistentVolumes left behind in aReleasedstate (relevant under aRetainreclaim policy).--remove-imagesis a no-op here — k8s images live in the registry, not the operator's host.--purge-allrefuses outright if any k8s install is detected on the host; tear k8s installs down individually or viakubectl delete namespace. wip-deploy export-cais not supported for k8s installs — see the command's own entry for thekubectl get secretalternative.
Gotchas
nukeis asymmetric between targets. On compose/dev, the defaultnuke(no flags) is non-destructive to data and secrets —compose downonly, no-v. On k8s, the defaultnukealways deletes the entire namespace viakubectl delete namespace, cascading to every namespaced resource including PVCs, regardless of whether--remove-datawas passed —--remove-dataonly extends the blast radius further, to cluster-scoped PVs that survive the namespace delete under aRetainreclaim policy. There is no "preserve data" k8s nuke short of not running it.--purge-allis host-wide and compose-scoped. It removes everywip-*container, pod, and network across every compose project on the host (plus, opt-in, every volume/image, and every install's secrets directory under~/.wip-deploy/). It refuses to run at all if any k8s install exists on the host — k8s teardown must go throughnuke --name <name>orkubectl delete namespacedirectly.- Mutation verbs require an existing install.
add-app,remove-app,app-deploy,redeploy,add-module,remove-moduleall load a persisteddeployment.deployer-state; there is no persisted spec untilinstallhas run at least once. installon top of a prior install can silently drop apps/modules — guarded, not silent. Re-runninginstallwith a reduced--app/--addset than a previous install for the same name aborts with an itemized list of what would be dropped, unless--reconcileis passed.remove-app/remove-moduleforce-remove the container directly (podman rm -f wip-<name>), ahead of the re-apply, so a dropped service doesn't linger as an orphan until the nextnuke.redeployre-renders from the persisted spec, not from fresh CLI flags — it exists specifically to pick up a deployer code change (renderer or spec-shape change) without the operator re-specifying every original install flag; the previous spec's values are otherwise untouched.- A failed apply that already mutated the running deployment still gets its new spec persisted — the install directory's saved state is meant to always describe the deployment as it actually is, even when the command that produced it reported failure.
export-careads live from the runningwip-caddycontainer, not from any file on the host — the install must be up, and Caddy must already have lazily generated its CA (triggered by any HTTPS request having hit it once).--tls internalsilently becomes--tls self-signedfor--target k8s. An operator who doesn't pass--tlsexplicitly on a k8s install still gets a generated self-signed cert + Secret, not Caddy's compose-only internal-CA mechanism.--apps-onlyrequires at least one--appand rejects combination with--add— every optional module depends on the core services that--apps-onlysuppresses.--namespace wiptyped explicitly alongside--name Xstill back-derives the namespace toX. Typer can't distinguish "the operator explicitly passed--namespace wip" from "namespace defaulted towip," so the--name→--namespacederivation fires whenever the namespace value is the literal defaultwip, even if it was typed on purpose. Pass a--namespacevalue other thanwipto opt out.import-bundlebundles are unsigned in this version — the CLI validates schema, expiry, and explicit read/write scope, but not authenticity; the operator is trusted to have received the bundle over a legitimate channel.redeploy's service-name scoping is compose/dev only. Passing service names on a k8s install is rejected outright with an error (kubectl applyis already incremental). The other scoped mutation verbs (add-app,remove-app,app-deploy) don't take a user-supplied service list at all — on k8s their internal compose-only scoping mechanism simply isn't invoked; every mutation there goes through the same incrementalkubectl apply.
