---
name: ember-deploy
description: Deploy and release container apps into Ember Sovereign Mode (attested confidential VMs) using the ember CLI. Use when working with ember.json, the ember CLI, @ember-sovereignty/provisioner, embersovereignty.com, or when the user mentions Ember deployments, releases, sealed secrets, egress manifests, or trust pages.
---

# Deploying and releasing on Ember

Ember runs a vendor's container image inside an attested confidential VM (CVM) whose only network exit is a policy proxy. The deployment's configuration is *measured*: image digest, plain env, secret env **names**, ports, and egress hosts are hashed into a measurement that is approved on-chain before anything boots. A config edit cannot widen access — any change to the measured spec is a new measurement requiring a new signed approval ("a release ceremony").

Human-readable companion: https://embersovereignty.com/onboarding/

## The one rule that prevents the most common mistake

**Secret values NEVER go in `ember.json`. Not in `env`, not anywhere.**

- `env` (object, `{"NAME": "value"}`) — plain settings. These values are **baked into the measured compose file**, which is public: it appears in the attestation evidence and on the deployment's trust page. Anything in `env` is disclosed to the world.
- `secretEnv` (array of strings, `["NAME"]`) — secret **names only**. At `ember deploy` time the CLI reads each name's **value from your shell environment**, encrypts it to the enclave's KMS-derived public key, and seals it into the CVM. The value never enters `ember.json`, the compose file, the measurement, or Ember's records. In the compose it appears only as a `${NAME}` placeholder.

So the flow for a secret is always:

```json
// ember.json — declare the NAME
"secretEnv": ["ANTHROPIC_API_KEY"]
```

```bash
# shell — provide the VALUE, then deploy in the same shell
export ANTHROPIC_API_KEY=sk-ant-...
ember deploy
```

If a name in `secretEnv` is not exported in the shell, `ember deploy` fails fast before anything is created ("secret env NAME not set in your shell"). There is no `.env` file support — only the process environment of the shell running `ember deploy`.

A name may not appear in both `env` and `secretEnv` (validation error: "declared both as plain env and secret"). All env names, plain or secret, must match `[A-Z][A-Z0-9_]*`.

## Prerequisites (one-time, human-in-the-loop)

These steps need a browser and a security key — walk the user through them, don't try to automate them:

1. **Create an org** at https://embersovereignty.com/console — CREATE ACCOUNT with a work email and org name; a code arrives by email. Email is the only sign-in method.
2. **Register a passkey** in the console. It becomes the owner of the org's smart account on Base; deployments are refused until one exists, and every release approval is signed with it. It is unrecoverable if lost — tell the user to register it in a synced provider (iCloud Keychain, Google Password Manager, 1Password).
3. **Install the CLI and log in:**

```bash
npm install -g @ember-sovereignty/provisioner
export EMBER_API=https://provisioner-production-b5b7.up.railway.app
ember login    # prints a URL + code; the user approves it in the console
```

`ember login` stores an org credential at `~/.config/ember/credentials.json`. Put the `EMBER_API` export in the user's shell profile — every command needs it.

If `npm install -g` reports `No versions available` (an org registry cooldown), install the exact tarball: `npm install -g "$(npm view @ember-sovereignty/provisioner dist.tarball)"`.

## ember.json

`ember init` scaffolds it in the current directory (refuses if one exists). Full shape:

```json
{
  "name": "acme-rag",
  "spec": {
    "image": "ghcr.io/acme/rag@sha256:9f2c…",
    "env": { "MODEL": "claude-sonnet-5" },
    "secretEnv": ["ANTHROPIC_API_KEY"],
    "ports": [8080],
    "storagePath": "/data",
    "egress": [{ "host": "api.anthropic.com", "port": 443 }],
    "resources": { "cpus": 8, "memoryMb": 16384, "diskGb": 100 }
  }
}
```

Field rules (the API validates strictly — an unknown or misspelled field is a hard 400):

- `name` — lowercase-kebab, 5–40 chars (`^[a-z0-9][a-z0-9-]{3,38}[a-z0-9]$`).
- `image` — **digest-pinned only** (`name@sha256:<64 hex>`); tags are rejected. Must be `linux/amd64` (on Apple Silicon: `docker buildx build --platform linux/amd64`) and **publicly pullable** without credentials. Get the digest from the push output or `docker buildx imagetools inspect <image:tag>`. Avoid expiring registries (ttl.sh) — an expired digest makes the deployment unrecreatable.
- `env` / `secretEnv` — see the rule above.
- `ports` — required, at least one. Each port is published at an attested HTTPS endpoint: port 80/443 at `https://<app-id>.<gateway>`, other ports with a `-<port>` suffix. Exact URLs come from `ember status` once running.
- `egress` — **required** (an empty array `[]` is valid and means no network exit at all). Exact hostnames, no wildcards, no IP literals; port 443 for anything added via the CLI. Egress is fail-closed: anything the image fetches at runtime — model weights included — must be listed or the proxy denies it, which looks like a hang from inside the app.
- `storagePath` — optional; mounts a persistent volume at that path.
- `resources` — optional; omit for the smallest tier (1 vCPU / 2 GB / 20 GB). Limits: cpus 1–16, memoryMb 512–32768, diskGb 10–200. Not part of the measurement, but set at create — changing it later means a new deployment.
- There is no command/args field: the image must boot straight to serving from its entrypoint with env-only configuration. If the entrypoint needs a subcommand, rebuild with it baked in (`ENTRYPOINT ["your-bin","serve"]`).
- `ember deploy` writes a `deploymentId` field back into `ember.json` — keep it; later commands default to it.

## Commands

```bash
ember init                          # scaffold ember.json
ember deploy                        # create + boot; secrets read from shell env, sealed
ember status [<id>]                 # state, endpoints, attestation evidence
ember list                          # all deployments for the org
ember egress add <hostname> [--wait]   # governed change: add an egress host (port 443)
ember release <name@sha256:digest> [--wait]  # governed change: new image version
ember verify [<id>]                 # check quote + on-chain measurement, trusting no one
ember trust enable [slug] | ember trust disable   # public trust page (on by default)
ember delete <id>                   # only removes records that never provisioned
```

## First deploy

1. Confirm the image runs locally first: `docker run` the **exact pinned digest** with the same env names — if it doesn't serve locally it won't serve in the CVM, and there are **no runtime logs** in the CVM to tell you why (by design; nothing readable leaves the enclave).
2. Write `ember.json`; export every `secretEnv` value in the shell.
3. `ember deploy`. The create ceremony answers in a minute or two; a large image then keeps pulling and booting for tens of minutes while the CLI follows status. Ctrl-C is safe (`ember status` resumes the view), and re-running `ember deploy` resumes the ceremony that already ran, by name.
4. `ember status` shows the attested HTTPS endpoint once running — smoke-test it. `ember status` proves the enclave runs the approved measurement, **not** that the app inside is healthy; app health is only ever the endpoint itself.

The first deploy's measurement is allowlisted as part of creation — the signing ceremony starts with the first *change*.

## Releases and other governed changes

`ember release <name@sha256:newdigest>` and `ember egress add <host>` both return `pending-approval` and change nothing yet. A human must then open the console's **Releases** view and sign the endorsement + approval with the org's passkey (gas is sponsored). The pipeline commits the moment the approval lands on-chain; `--wait` makes the CLI follow it. When the buyer's institution holds the governing account, their approval runs instead.

- **Changing env rides a release too.** To change plain env values or secret-env *names*, edit `ember.json` and ship it with the next `ember release` — there is no env-only verb, and a release needs a new digest (rebuilding the same image with a bumped label is enough).
- **Rotating a secret value is not self-serve.** Sealed values are set at first deploy and survive releases unchanged (`update_env_vars: false` on upgrade). To rotate one, the vendor must contact Ember via https://embersovereignty.com/briefing/.

## Debugging inside the no-logs constraint

- Egress fail-closed is the first suspect for a hang: check the manifest before suspecting a crash.
- If the entrypoint is a process manager (supervisord, s6, a shell script), children may not inherit env the way local `docker run` suggested — the classic silent crash-loop. Have PID 1 capture env to a file (`export -p > /run/app.env`) and have every service source it.
- Build a health endpoint that reports readiness and dependency state before deploying; it's the only observability you get.
- Every fix discovered in production costs a full release ceremony — exercise every route locally first, not just the landing page.

## Verification

`ember verify` fetches the attestation quote, checks its signature against the silicon vendor's root of trust, and compares the measured deployment to the on-chain approved list — trusting neither the vendor's infrastructure nor Ember's records. For a fully independent quote, set `PHALA_CLOUD_API_KEY` (free Phala Cloud account) and it is fetched live from the CVM platform. Credential-free mode: `ember verify --app 0x… --quote <file|url>`.

Every deployment gets a public trust page (auto-assigned slug, shown at deploy). `ember trust disable` opts out and sticks across releases.
