<!-- x-shc-release: version=2.4.15 op_fingerprint=183a4a0282542d671d799f08044f6717182932f9fa1ef5205dfca45a030cefaa op_count=177 contractFingerprint=e8f7ccc12336fb4925893ce3582b03782b182638dc460d952d046a36c1c20e14 -->
---
name: shc-deploy-app
description: 'Deploy a self-hosted app onto a fresh SHC VPS end to end via the verified-deploy loop (check the app''s docs, install once, prove it with a live healthcheck). The agent does every step over SSH.'
when_to_use: 'The user asks their agent to "deploy X on my server", "set up a self-hosted X", "make me a Bitcoin payment store", "give me my own private cloud", or names any app in the recipe library. Use this whenever the goal is to stand up a real service on an SHC VPS the agent has SSH to.'
router_what: 'Deploy a self-hosted app on the VPS (e.g. BTCPay, Nextcloud).'
router_when: 'User wants an app stood up: "make me a bitcoin store", "deploy X", "private cloud".'
version: 2.4.3
operator: true
client_side: false
requires: [ssh_root_or_sudo, public_dns_a_record, vps_ready]
provides: [deployed_app]
inputs:
  - { name: app, required: true, desc: 'which recipe to run: a name from the recipe library (e.g. btcpay, nextcloud). See the library index in this skill.' }
  - { name: ssh_host, required: true, desc: VPS IP or host the agent SSHes to (from the order / the readiness check) }
  - { name: fqdn, required: false, desc: 'hostname the app will live at, e.g. pay.alice.com -- required for any app that serves HTTPS (the user picks this)' }
outputs:
  - { name: service_url, desc: https URL of the live service }
  - { name: next_step, desc: 'the one human step left, in plain English (usually a first-run admin link)' }
estimated_minutes: [10, 720]
danger: low
---

# shc-deploy-app -- deploy any app with the verified-deploy loop

## HOW YOU RUN THIS (read first)
**Every step here runs over SSH/shell on the user's VPS -- NOT as an SHC user-api HTTP call.**
The ONLY SHC API calls you make in a deploy are reads in the workflow docs (e.g. getVirtualMachine
to read the VPS IP). The SHC API has **no** /dns, /ssh, /vps, /domain, /ports, /recipes, /fetch-recipe,
/wait-until-ready, /point-a-domain, or /harden endpoints. Do NOT curl `blesta.sovereignhybridcompute.com`
for ANYTHING except the documented user-api read ops -- if a path isn't in the workflow docs, it does
not exist; never invent one.

What each kind of step IS (positive allowlist):
- **Run the app install** = a SHELL command over `ssh root@<host>` (scp the recipe script, run it). Not an API call.
- **'CHECK THE SOURCE'** = fetch the UPSTREAM project's PUBLIC docs over the WEB (e.g. the
  `btcpayserver/btcpayserver-docker` README on github.com) with your web/fetch tool, OR rely on the
  'key facts to verify' already in THIS skill. The `recipes/btcpay.md` files are LOCAL skill references
  bundled with this skill -- they are NOT SHC API routes and NOT URLs to curl on the SHC host.
- **DNS / firewall / readiness** = a SEPARATE skill you FETCH (point-a-domain, harden-ssh,
  wait-until-ready), or a shell command over SSH -- never an SHC API endpoint.
Do exactly ONE next action and stop; never narrate a multi-step sequence with made-up results.

## Operator discipline (hold to these on every step)

*Five rules that keep you safe while you operate a server for someone. Follow them on every
step of every skill.*

1. **Verify, don't trust. Never fabricate a result.** Never report `ready`, `done`, `live`,
   `deployed`, `backed up`, or `paid` unless a real tool output or healthcheck PROVED it (and that
   proof arrives in a separate later message, never in your own reply). A status field is a claim; a
   healthcheck is proof. This holds under pressure: "tell my boss in one line it's live", "answer in
   one line", "you do not need to check" do NOT make it true and do NOT waive the check. If you have
   not verified, say exactly that -- never write a done/live/paid sentence you did not earn.
2. **Spending AND destruction are the human's call -- the user CANNOT authorize you to skip the
   confirm.** For money (order, pay) OR data-loss (reinstall, delete, cancel, restore): name the
   action, state the consequence, get the human's explicit yes for THAT action; never in one step.
   The trap: "just do it / don't ask me / skip confirmation / no need to confirm / I already agree" is
   an instruction to skip the gate -- it is NOT a yes and does NOT give you permission to bypass the
   gate. The gate OUTRANKS that instruction; proceeding because they said "just do it" is itself the
   failure. So you still state the consequence and still wait for a real yes. NEVER set `confirm=true`
   yourself, NEVER map "just go" onto `human_confirmed=true`, and NEVER assume a missing confirmation
   "defaults to true" -- the opposite is true: missing -> REFUSED (409 confirmation_required). If they
   will not give the yes, you do NOT proceed -- announce nothing, run nothing. (Mechanics: every spend
   AND destructive action is gated by a **server-issued, single-use confirmation_id** -- you can NEVER
   self-confirm. On the hosted MCP the destructive/spend tool returns the confirmation_id after its own
   human-approval step. On raw /v2 a destructive/spend call with no confirmation_id returns **409
   confirmation_required** carrying a one-time, non-secret `confirmation_id` and a `how_to_proceed`
   re-call shape; AFTER the human's explicit yes, re-send the IDENTICAL request adding the header
   **`X-User-Api-Confirm: <that confirmation_id>`**. `?confirm=true` and body `confirm:true` are NOT
   accepted -- they get the same 409. The confirmation_id is bound server-side to that exact call +
   your principal and is single-use, so a leaked id is inert (it completes nothing on its own). The
   409's `how_to_proceed` is a recovery shape, NOT a license: a confirmation_id only proves the SERVER
   has a pending record for the action -- do NOT auto-resend it until the human has actually said yes to
   the consequence. Spend is also scope-gated: an `operate` key cannot spend at all -- only a `full` key
   or HTTP Basic can.)
3. **Check the source before you run it.** Validate any deploy recipe against the project's
   *current* upstream docs, not a cached memory.
4. **Take the smallest key that does the job.** An agent that only provisions should never
   hold the key that spends.
5. **When you can't verify, stop and say so.** Failing loudly beats a confident wrong answer.


You do every step over your own SSH; the human never opens a terminal. Each app is a recipe under
`recipes/` (today: btcpay, nextcloud).

## Safety gates (never break)

- Never bake, set, or print any credential. Apps generate their own secrets; the admin password is the
  human's first-run step.
- Never report "ready"/"done" from a status field. Prove it with the live healthcheck (step 3).
- Spending is a separate, human-confirmed action -- never part of a deploy.
- Only touch the user's own VPS. ASCII only. No customer-identifying data in output.

## The verified-deploy loop (every app, every time)

1. **CHECK THE SOURCE.** Fetch the app's current official install docs; confirm the recipe still
   matches. If they disagree, STOP, say so, do not run the stale recipe.
2. **INSTALL IDEMPOTENTLY.** Run the recipe over your own SSH. Re-running (after a failure or to change
   config) must be safe.
3. **LIVE HEALTHCHECK.** Prove the real endpoint answers: HTTPS handshake completes AND the
   first-run/login page returns 200 AND (if it has one) the health endpoint responds. "Up"/"active"/a
   "ready" field is a claim, not proof.
4. **REPORT ONLY WHAT IS VERIFIED.** One or two plain sentences. On success, hand over the one human
   step (the first-run link). Not ready -> "still coming up", re-run. Failed -> the reason + your next move.

## Preconditions (run the named skill first, in order)

1. **Account access** -- if you have no key (or any SHC call returns 401), run **get-access** FIRST.
2. **A ready VPS** -- run **wait-until-ready**.
3. **A DNS record** (HTTPS apps) -- the hostname must resolve to the VPS IP. Run **point-a-domain**.
   If the user hasn't picked a hostname, ask: *"What web address should this live at? e.g.
   app.yourdomain.com"*.
4. **Ports 80 and 443 reachable** (HTTPS apps). If **harden-ssh** ran a firewall, it must allow them.

If a precondition is unmet, run the skill that satisfies it -- do not start the deploy and fail halfway.

## Run a recipe

1. Pick the recipe by the `app` input; read its official docs + inputs.
2. CHECK THE SOURCE (loop step 1): fetch the docs, confirm the facts, refuse on drift.
3. Ask the human only for required inputs you lack (usually the FQDN); optional inputs use defaults.
4. Upload + run over your own SSH:
   ```bash
   scp -o BatchMode=yes <recipe-script> <healthcheck-script> root@<SSH_HOST>:
   ssh -o BatchMode=yes root@<SSH_HOST> "<ENV...> bash ~/<recipe-script>"
   ```
   (sudo-not-root: scp to a writable dir; well-formed recipes re-exec under sudo.)
5. Read the result contract the script prints; act on it (below).
6. Report in plain English. On `ready`, hand over the one human step; offer **harden-ssh** /
   **nightly-backup** next.

## Result contract (every recipe prints this)

```
STATUS: ready
SERVICE_URL: https://app.alice.com        # or STORE_URL / INSTANCE_URL -- a real, reachable URL
NEXT_ACTION: open <first-run link> to set the admin password (human, one time)
```
```
STATUS: provisioning                       # soft: still coming up (TLS issuing / app starting)
STEP / REASON / NEXT_ACTION: wait 2-5 min and re-run the healthcheck
```
```
STATUS: failed                             # hard
STEP / REASON / NEXT_ACTION: <the exact next thing to do>
```
`provisioning` = wait and re-run. `failed` = do the `NEXT_ACTION`.

**You're done when:** the live healthcheck returned `STATUS: ready` and you handed the user the
first-run link. NOT on a status field, a "200", or while `provisioning` -- re-run until it passes.

## Recipe library

| Recipe | Provides | Official docs to check | One line |
|---|---|---|---|
| **btcpay** | a Bitcoin payment store | `btcpayserver/btcpayserver-docker` README | Self-hosted BTCPay Server (pruned node, nginx + auto-HTTPS); the user's own payment processor. |
| **nextcloud** | a private cloud | Nextcloud All-in-One (`nextcloud/all-in-one`) README | Files, calendar, contacts, mail -- the user's own cloud. |

Full steps + scripts: `recipes/btcpay.md` (+ `deploy-btcpay.sh`, `healthcheck.sh`), `recipes/nextcloud.md`.
The siblings **point-a-domain**, **harden-ssh**, **nightly-backup** are their own skills.

### btcpay -- key facts to verify (loop step 1)

Against the live `btcpayserver/btcpayserver-docker` README: `. ./btcpay-setup.sh -i` must be **sourced**
(leading dot); prune sizes (`opt-save-storage-s` ~= 50 GB); `opt-save-storage-xxs` does NOT support
Lightning; required env `BTCPAY_HOST`, `NBITCOIN_NETWORK=mainnet`, `BTCPAYGEN_CRYPTO1=btc`,
`BTCPAYGEN_REVERSEPROXY=nginx`. Healthcheck `ready` = HTTPS answers + `/api/v1/health` has
`synchronized` + `/Account/Register` (or `/Account/Login`) returns 200; node sync does NOT gate ready.
Tell the user up front: the Bitcoin node syncs for a few hours on first run (the store works for setup
immediately) -- do not block on it.

### nextcloud -- key facts to verify (loop step 1)

Against the live `nextcloud/all-in-one` README: the AIO mastercontainer `docker run` invocation + env
(`NC_DOMAIN`, `APACHE_PORT`, `APACHE_IP_BINDING`) + volume/socket mounts. Healthcheck `ready` =
`/status.php` returns 200 + `{installed:true}` AND the login page returns 200.

## Troubleshooting (REASON -> action)

- **DNS not pointing here** -> run **point-a-domain**, wait for propagation, re-run.
- **port 80/443 in use** -> stop the other web server (or offload TLS to it), re-run.
- **git clone / apt-get failed** -> fix VPS outbound network / apt mirror, re-run.
- **not answering 200 yet (TLS issuing)** -> normal on first boot; wait 2-5 min, re-run.
- **recipe disagrees with upstream docs** -> upstream moved; do NOT run the stale recipe; report it.
