> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nanny.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploying a governed app

> The ordered path from a working app to a governed one running on a host, and how to read the log it prints on the way up.

This page is the sequence. [Governance server](/v0.7/guides/governance-server)
is the reference for everything it touches: certificate operations, token
rotation, port behaviour, server management. Follow the steps here, and read
that page when a step raises a question.

## 1. Decide the shape

There are two, and the choice decides everything after it.

|                          | Shape                                                                                      | What it needs                          |
| ------------------------ | ------------------------------------------------------------------------------------------ | -------------------------------------- |
| **One container**        | Your app runs under a governor in the same container.                                      | Nothing. `nanny run` is already this.  |
| **Governor and joiners** | One service runs the governor, others connect to it from their own containers or machines. | mTLS certificates and a session token. |

Start with one container if you can. Not everything has to move at once: a
second process joins later without the first one changing shape, and the rule
set it is evaluated against is the same one.

<Note>
  **`nanny run` is the same command here and on your laptop.** It brings up a
  governor and runs your app underneath it; on loopback that needs no
  certificates and no setup. There is no second, quieter mode to develop
  against and then swap out, so the shape you tested is the shape you deploy.
</Note>

## 2. Install dependencies at build time

A governed run should not fail on something unrelated to its policy. The
allowlist governs your app's tool calls, and a package installer running inside
a governed process is a surprising way to discover that.

```dockerfile theme={null}
RUN uv sync --locked        # or pip install -r requirements.txt, npm ci, …
```

<Warning>
  `--locked`, not `--frozen`. Both refuse to update the lock file; only
  `--locked` first checks that the lock still agrees with `pyproject.toml`.
  With `--frozen`, a lock left behind by an edited `pyproject.toml` installs
  anyway, so an image can ship an older `nanny-sdk` than the one you asked
  for and fail at runtime on a symbol that exists in the version you thought
  you had. `npm ci` and `pip install -r` already fail this way by default.
</Warning>

## 3. Put the runtime in the image

Pin it. The installer at `install.nanny.run` fetches the **latest** release,
which is right on a laptop and wrong in an image: rebuild in three months and
you silently ship a different runtime.

```dockerfile theme={null}
ARG NANNY_VERSION=0.7.0
RUN curl -fsSL "https://github.com/nanny-run/nanny/releases/download/v${NANNY_VERSION}/nanny-linux-x86_64.tar.gz" | tar -xz -C /usr/local/bin nanny
```

Keep that version and your SDK floor in step. A lock file that installs an
older SDK than the runtime expects fails at the first governed call, not at
build time.

## 4. Set one secret

```bash theme={null}
NANNY_API_KEY=nny_...
```

Create it in the dashboard under **Settings → API Keys**, then set it however
your platform sets secrets: a Fly secret, a Kubernetes `Secret`, a Coolify
environment variable. That single variable decides whether runs sync; see
[Connect to Nanny Cloud](/v0.7/guides/managed-mode).

The key's prefix (`nny_live_` or `nny_sdbx_`) also decides whether this
deployment writes real evidence or throwaway sandbox data. Read
[Live and sandbox](/v0.7/guides/environments) before deploying against a live
key for the first time.

Leave it unset and everything still works. Enforcement is local and never
depends on the cloud.

<Warning>
  Do not bake the key into an image or commit it. It is an ordinary deployment
  secret and belongs with the rest of them.
</Warning>

Set it on the governor only. Joined processes report through the governor, and
a second key would split the audit trail and count the same run twice.

## 5. If anything joins from elsewhere, do this before you deploy

Skip to step 7 for a single container.

Generate the bundle **on your machine, never in a container**, from inside the
project, and mint the token at the same time:

```bash theme={null}
nanny certs generate --live --san governor --days 3650
openssl rand -hex 32
```

`--live` picks which certificate authority you are issuing from. Omit it and
you get the sandbox one. **Never point a production governor at a sandbox CA:**
a CA is what the governor trusts, so a client certificate issued for staging
would be admitted to production, and the governor could not tell, because the
signature is genuinely valid. Two environments, two authorities.

`--san` must cover the name the joiner dials. The client verifies the name it
dialled against that list, and a mismatch fails the handshake before any
request is made.

Then mount, rather than paste: `server.crt`, `server.key` and `ca.crt` into the
governor, and `client.crt`, `client.key` and `ca.crt` into each joiner. A value
in the environment is readable through `/proc/<pid>/environ` and inherited by
every child process, where a mounted file can be `0600` and is inherited by
nothing. Only files rotate, too: an environment variable cannot change in a
running process, so certificates supplied that way are replaced on the next
restart instead of in place.

`ca.key` never leaves your machine. It signs certificates, so anyone holding it
can mint a client the governor will trust.

The session token is the exception and is always an environment variable, on
both sides. Both ends read the same variable, so a second form to interpret is
only a way for them to disagree. See
[Rotating the token](/v0.7/guides/governance-server#rotating-the-token) for
moving a fleet onto a new one without stopping it.

## 6. One image, both shapes

A `CMD` with certificate paths baked into it only runs *with* certificates, so
a single-container deploy needs a second image. Deciding at boot from whether
the certificates are present gets both shapes out of one:

```sh theme={null}
#!/bin/sh
set -e

# Fixed here rather than configured: the deployment maps a source path it
# chooses onto a destination this file owns, and there is no variable to set
# wrong. Outside /app deliberately, which is build output.
CERT_DIR=/run/secrets/nanny

if [ -f "$CERT_DIR/server.crt" ]; then
  exec nanny run --addr "0.0.0.0:62669" \
    --cert "$CERT_DIR/server.crt" \
    --key  "$CERT_DIR/server.key" \
    --ca   "$CERT_DIR/ca.crt"
fi

echo "nanny: no certificates at $CERT_DIR, serving on loopback only"
exec nanny run
```

The two branches match how the runtime already behaves: it picks its transport
from the address, serving plain HTTP on loopback and requiring mTLS anywhere
else. So binding `0.0.0.0` without certificates would fail to start, and
binding loopback in a container makes the governor unreachable by anything but
itself. Adding the certificates is what opens it to the joiners.

<Note>
  **`exec` is what makes this safe.** Nanny must be PID 1 so it receives
  `SIGTERM` directly, drains for up to 10 seconds, and stops the app it
  launched. A shell that stays running as the parent breaks that: `sh` does not
  forward signals to its children, so the governor never drains and nothing
  notices if one half dies while the other keeps running half-governed.
  `exec` replaces the shell rather than leaving one in front, so a script that
  ends in `exec nanny` is exactly as correct as a plain `CMD`.
</Note>

## 7. Read the first boot

```
nanny: mode managed, syncing to https://api.nanny.run (app: your-app)
nanny: governance server started
  address      : 127.0.0.1:62669
  session token: 34e6beee…8fad (64 chars)
  token file   : /root/.nanny/servers/app_…/server.token
nanny: running [start] under this governor (plain HTTP, loopback)
```

Four things worth checking, in order:

* **`mode managed`** means the API key was read. `mode local` means it was not,
  and nothing will sync.
* **`address`** is what you asked for. `127.0.0.1` when you expected `0.0.0.0`
  means the certificate branch above did not fire.
* **`session token`** is a fingerprint, never the token: eight characters from
  the head, four from the tail, and the length. Enough to confirm the governor
  took the token you set and not a stale one, and not enough to use, so a log
  aggregator holding it forever costs you nothing. Compare it against what you
  set on the joiners. Under mTLS the governor also prints the variables a
  joining process needs.
* **The launch line** names the transport. `(mTLS)` here and `(plain HTTP,
  loopback)` are the two you will see.

Then `nanny status` from the same directory, or the dashboard, which lists the
app by the name in `.nanny/app.json`.

**Start order does not matter.** A joiner started before its governor retries
its first connection for 30 seconds rather than failing the work it was handed.
Once it has connected, a later failure stops the run immediately: waiting for a
first connection is patience, waiting mid-run is running ungoverned.

## 8. When it does not come up

| What you see                                    | What it is                                                                                                                                                                                                                                                                                                                                                       |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Exits immediately, names a missing certificate  | The governor refuses to start rather than serve a non-loopback address unprotected. Check the mount actually landed: an empty directory and a missing mount look identical from inside.                                                                                                                                                                          |
| Exits on a busy port                            | A port you named explicitly is an error, not a silent move to another one. A governor listening somewhere the firewall does not expect is worse than one that refuses to start. The default port moves on its own and says so.                                                                                                                                   |
| A joiner gets `401`                             | The tokens differ. Compare the governor's printed fingerprint against the joiner's value; a trailing newline in a secret manager is the usual cause.                                                                                                                                                                                                             |
| A joiner fails the handshake before any request | `--san` did not cover the name it dialled. Regenerate with the name in the list; the client checks this, so the governor's log may show nothing at all.                                                                                                                                                                                                          |
| `state directory not writable`                  | Not a failure. The governor records its address, pid and token so processes *on the same machine* can find it with `--join`, `status` and `stop`. It says so once and serves anyway, so a read-only root filesystem runs a governor perfectly well. A process joining from another machine is given the address and token as configuration and never reads them. |
| The governor exits when your app does           | By design. The governor's job is that process; there is nothing to govern once it is gone.                                                                                                                                                                                                                                                                       |

## 9. After it is up

`/health` is served without the session token and reports whether the run is up
and nothing else, so an orchestrator can probe it:

```
GET http://governor:62669/health  →  {"state":"running"}
```

`/status` carries call counts, history and the stop reason, and stays behind the
token.

There is no login step and no state to carry between deploys. A fresh container
behaves the same as one that has been running for a month, and twenty replicas
behave the same as one.

If the cloud is unreachable, events are held under `.nanny/spool/`
(partitioned by environment) and delivered on the next run. If your platform
gives the container a writable filesystem this is automatic; if it does not, a
cloud outage costs those events, and everything else still works.
