← Back to Library

Deployment (wip-deploy)

cli ·  operators
cliwip-deploydeploymentoperators

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)

FlagDefaultControls
--preset, -pstandardPreset name: core | headless | standard | analytics | full (see Presets below).
--target, -tcomposeDeployment target: compose | k8s | dev.
--variantdevSecurity 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.
--hostnameNonelocalhost if --target dev, else wip.localExternal hostname as seen by browsers.
--tlsinternalTLS mode: internal | letsencrypt | external | self-signed. internal is auto-upgraded to self-signed when --target k8s (a cert + Secret are generated pre-install).
--https-portNone443 (k8s) / 8443 (compose, dev)External HTTPS port.
--http-portNone80 (k8s) / 8080 (compose, dev)External HTTP port.
--data-dirNone<repo-root>/dataCompose data directory (compose target only).
--namespacewipK8s 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-classrook-ceph-blockK8s StorageClass for PVCs.
--ingress-classnginxK8s IngressClass name.
--tls-secret-namewip-tlsK8s TLS Secret name.
--dev-modesimpleDev mode: simple (compose + source mounts + --reload) | tilt (reserved, not implemented).
--registryNoneImage registry (e.g. ghcr.io/you).
--tagNoneDeployment-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-wipNoneURL 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-onlyFalseInstall 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-modeNoneOverride auth mode: oidc | api-key-only | hybrid.
--auth-gateway / --no-auth-gatewayNoneOverride the auth gateway on/off.
--secrets-backendNonefileSecret backend. Only file is implemented for every target (including k8s — the k8s renderer bakes file-backend values into an in-cluster Secret).
--secrets-locationNone~/.wip-deploy/<name>/secretsSecret backend location (for file/sops backends).
--repo-rootNone (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.
--nameNone → the --namespace value for --target k8s, else defaultDeployment 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

PresetOptional modules enabledAuthNotes
coremcp-serverapi-key-only, gateway offMinimal API, no OIDC. Local dev, demos, trusted-network installs. Apps are opt-in via --app.
headlessmcp-serverapi-key-only, gateway offSame shape as core — lowest footprint, no human-facing UI surface. Backend-for-another-system, MCP-only workflows, CI fixtures.
standardreporting-sync, minio, mcp-serverhybrid, gateway onRecommended 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.
analyticsreporting-sync, mcp-serverhybrid, gateway onstandard's reporting pipeline (Postgres + reporting-sync) without file storage — --add minio to add it.
fullreporting-sync, ingest-gateway, minio, mcp-serverhybrid, gateway onEverything: 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

wip-deploy show-spec

wip-deploy render

wip-deploy install

wip-deploy status

wip-deploy rebuild

wip-deploy restart

wip-deploy up

wip-deploy stop

wip-deploy start

wip-deploy register-app

wip-deploy unregister-app

wip-deploy validate-manifest

wip-deploy add-app

wip-deploy remove-app

wip-deploy app-deploy

wip-deploy redeploy

wip-deploy add-module

wip-deploy remove-module

wip-deploy nuke

wip-deploy check-app-deployability

wip-deploy export-ca

wip-deploy import-bundle

wip-deploy examples

Workflows

dev

Local iteration against source, hot-reloading Python services via bind mounts.

  1. wip-deploy install --target dev --preset standard — first install. The dev renderer materializes each component's build context into the rendered tree (patching in wip-auth, and for document-store also the WIP-Toolkit dependency, the way the production build script does), bind-mounts components/<name>/src read-only into the container, and appends --reload to Python services' uvicorn commands. Node/Go apps get a build context but no hot reload. Every install with target=dev forces compose up -d --build --force-recreate, and — unless the apply is scoped to a subset of services — first force-removes any stray wip-* 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.
  2. 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-app lets an operator register a checkout's path once instead of retyping --app-source on every install.
  3. Iterate: edit component source → Python services pick it up live via --reload; for a Dockerfile/requirements.txt/package.json edit, wip-deploy rebuild <service>; for an env-var-only change, wip-deploy restart <service> (no rebuild).
  4. 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>.
  5. wip-deploy status to see what's running and healthy.
  6. 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 printed wip-deploy install --target dev --apps-only --remote-wip <url> --app <name>... — no local core services, apps talk to the remote WIP directly.
  7. Post-reboot / Podman-machine restart: wip-deploy up — brings back exited containers with no rebuild and no re-render.
  8. Pause/resume without deleting anything: wip-deploy stop / wip-deploy start.
  9. Teardown: wip-deploy nuke (add --remove-data to also drop volumes).

compose

Production-style single-host deployment via podman-compose / docker compose.

  1. wip-deploy install --hostname wip-pi.local --preset standard (self-signed TLS by default) or wip-deploy install --hostname wip.example.com --tls letsencrypt for a public hostname.
  2. install assembles the spec, discovers manifests, runs preflight (port-free + stale-container checks), resolves/generates secrets, renders docker-compose.yaml + .env + config/caddy/Caddyfile (when the auth gateway or OIDC is active) + config/dex/config.yaml (when Dex is active), then compose up -d and waits for every service with a healthcheck to report healthy (default 300-second budget; on timeout the install fails unless spec.apply.on_timeout says otherwise). Post-install hooks run via compose exec once health is confirmed.
  3. The applied spec is persisted to deployment.deployer-state in the install directory, so later commands don't need the original flags repeated.
  4. Day-2 operations: wip-deploy status for a health table; wip-deploy rebuild <svc> after an image change; wip-deploy restart <svc> for an env-only change; wip-deploy redeploy to pick up a deployer renderer/spec change and recreate only what actually changed; wip-deploy add-app / remove-app / add-module / remove-module for 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.
  5. wip-deploy export-ca --out <file> (for --tls internal installs) so off-host clients can trust the Caddy-generated CA.
  6. Pause/resume: wip-deploy stop / wip-deploy start. Post-reboot recovery of exited containers: wip-deploy up.
  7. 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.

  1. wip-deploy render --target k8s --namespace wip --preset standard to inspect the generated manifests first (optional but recommended before a first apply).
  2. wip-deploy install --target k8s --namespace wip --tls external — requires kubectl already configured against the target cluster. --tls internal (the flag's own default) is silently upgraded to self-signed for this target; the install pre-flight then idempotently ensures a kubernetes.io/tls Secret exists, generating an RSA-4096 self-signed cert via openssl on 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.
  3. Apply cleans any previously rendered top-level *.yaml and the services//infrastructure/ subdirectories under the install directory (without touching a secrets/ backend directory that may share the path), writes the fresh tree, applies namespace.yaml first without pruning (Namespace is cluster-scoped — pruning it could touch a peer WIP namespace), then applies the rest of the tree with kubectl apply -R -f . --prune --selector=app.kubernetes.io/part-of=wip scoped 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 via kubectl rollout status per workload (a StatefulSet for anything with declared storage, a Deployment otherwise) up to the shared spec.apply.timeout_seconds budget, then runs post-install hooks via kubectl exec against a Running pod selected by the component's name label.
  4. Day-2: wip-deploy status --namespace <ns> (or --name <install> if the target/namespace were auto-detected from the saved state) for a kubectl get pods-derived table; wip-deploy status --namespace <ns> --diff re-renders from the persisted spec and runs kubectl diff against 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 apply is already incremental.
  5. 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.
  6. 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-data additionally deletes any cluster-scoped PersistentVolumes left behind in a Released state (relevant under a Retain reclaim policy). --remove-images is a no-op here — k8s images live in the registry, not the operator's host. --purge-all refuses outright if any k8s install is detected on the host; tear k8s installs down individually or via kubectl delete namespace.
  7. wip-deploy export-ca is not supported for k8s installs — see the command's own entry for the kubectl get secret alternative.

Gotchas

See also

Generated from real source code, not hand-written — see the WIP Technical Library.