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

# Deployment

> How the coordinator gets built, attested, and run on EC2 inside a Nitro Enclave

## Build (CI on every push)

The Containerfile is a **stagex** multi-stage build, chosen specifically so
the result is reproducible — this is the prerequisite for the
[Nautilus](/architecture/decentralization#1-nautilus-nitro-enclave-making-the-coordinators-code-trustless)
claim that anyone can rebuild the source and get the same PCRs:

```
1. Pinned stagex base images (musl toolchain, rust, busybox, socat)
2. node:22-alpine build stage → musl-linked node binary + libstdc++/libgcc_s
3. npm ci --omit=dev against scripts/package.json (pinned sidecar deps)
4. cargo build --release --target x86_64-unknown-linux-musl
   for init, coordinator, and the support crates
5. Initramfs assembly: kernel module, busybox, socat, Rust binaries,
   node binary + libs + scripts dir
6. eif_build → coordinator.eif + coordinator.pcrs
```

<Tip>
  Rebuilding the same source tree produces the same `coordinator.pcrs`
  byte-for-byte. Those PCRs are what `EnclaveConfig<ENCLAVE>` is updated to
  via `update_pcrs` before the first `register_enclave` succeeds — see
  [On-chain contracts](/architecture/onchain).
</Tip>

## EC2 deployment

Triggered by every push to `main`. Workflow: `.github/workflows/deploy.yml`.

```
GitHub Actions
  ├─ scp source to EC2
  ├─ write ~/.env.runtime from PINAIVU_ENV_FILE secret
  ├─ install docker, nitro-cli, socat, jq, node, npm on EC2 (idempotent)
  ├─ docker build → coordinator.eif + coordinator.pcrs in ./out
  ├─ stop previous bridges, pkill any leftover manual socats
  ├─ terminate previous enclave, compact memory, restart allocator
  │                        (NOT just `start` — restart so memory_mib changes apply)
  ├─ nitro-cli run-enclave --cpu-count 2 --memory 4096 --eif-path …
  ├─ register systemd units for the host-side socat bridges:
  │       pinaivu-bridge-http        TCP:4000 → VSOCK:CID:4000
  │       pinaivu-bridge-libp2p      TCP:4001 → VSOCK:CID:4001
  │       pinaivu-logs               VSOCK:5000 → /tmp/coordinator.log
  │       pinaivu-outbound-postgres  VSOCK:8101 → TCP:supabase
  │       pinaivu-outbound-redis     VSOCK:8102 → TCP:upstash
  ├─ awk 1 ~/.env.runtime ~/.env.runtime.dynamic | socat - VSOCK:CID:7000
  │                       # concatenation must be newline-terminated — see
  │                       # the .env.runtime vs .env.runtime.dynamic note below
  ├─ wait for /health, then re-check uptime after 60s
  │                       (catches coordinators that pass /health then die)
  └─ register coordinator on Sui via scripts/register-coordinator.sh
     (full steps in On-chain contracts → Coordinator registration)
```

## Inside the enclave at boot

From `src/init/src/main.rs`:

```
init (PID 1) runs:
  1. Mount /proc, /sys, /dev, /tmp, cgroups
  2. NSM heartbeat (signals nitro-cli the enclave is alive)
  3. Insert nsm.ko, seed kernel entropy
  4. Bring up loopback
  5. vsock_accept(7000) — blocks until parent pushes the env file
  6. Apply env defaults (PINAIVU_BIND, PINAIVU_LIBP2P_LISTEN, ...)
  7. Generate SIDECAR_SECRET if not set (random 32 bytes from NSM RNG)
  8. Write /etc/hosts entries mapping the real Postgres/Redis/Sui RPC
     hostnames to 127.0.0.1 (TLS SNI must see the real name; bridge
     forwards bytes)
  9. socat bridges:
       TCP-LISTEN:5432   → VSOCK:3:8101   Postgres outbound
       TCP-LISTEN:6379   → VSOCK:3:8102   Redis outbound
       TCP-LISTEN:443    → VSOCK:3:8103   Sui RPC outbound (HTTPS)
       VSOCK-LISTEN:4000 → TCP:127.0.0.1:4000   HTTP inbound
       VSOCK-LISTEN:4001 → TCP:127.0.0.1:4001   libp2p inbound
 10. Truncate /tmp/coordinator.log; open append-only for both
     sidecar and coordinator (O_APPEND on both writers avoids
     race-corrupting each other's line-sized appends)
 11. Spawn TS sidecar on 127.0.0.1:8200 (loopback only) — reads
     PINAIVU_ENCLAVE_OBJECT_ID at startup so it can settle without an
     HTTP push if the dynamic env file already holds an id
 12. Spawn coordinator binary; stdout+stderr → /tmp/coordinator.log
 13. log_forwarder thread re-opens the file every ~100ms, tracks its own
     position, writes new bytes to VSOCK:5000 via libc::send(MSG_NOSIGNAL)
 14. wait(coordinator)
 15. reboot()   # exits the enclave on any process exit
```

### `~/.env.runtime` vs `~/.env.runtime.dynamic`

* `~/.env.runtime` is **overwritten on every deploy** from the
  `PINAIVU_ENV_FILE` GitHub Actions secret. Static config.
* `~/.env.runtime.dynamic` is written by post-boot host scripts (e.g.
  `register-coordinator.sh`). Holds values discovered after enclave
  start — currently just `PINAIVU_ENCLAVE_OBJECT_ID`. Survives across
  deploys.
* The VSOCK:7000 push concatenates both with `awk 1` so every line ends
  in `\n` — a plain `cat A B` can glue the last line of A onto the first
  line of B, silently corrupting whichever variable sits at that boundary.

## Local dev

Running the coordinator outside an enclave still works (mock NSM
attestation):

```bash theme={null}
cd ~/projects/pinaivu/coordinator
DATABASE_URL=postgres://... REDIS_URL=redis://localhost:6379 \
SIDECAR_SECRET=local-dev \
cargo run -p coordinator
```

Skip the sidecar in dev — registration just logs a warning and the HTTP
server keeps serving. The inference flow doesn't depend on it.

## Smoke testing prod

```bash theme={null}
EC2_IP=<your IP>
curl -s http://$EC2_IP:4000/health           # → "ok"
curl -s http://$EC2_IP:4000/enclave_health   # → { pubkey, peer_id, uptime_ms }
```

<CardGroup cols={2}>
  <Card title="Environment variables" icon="key" href="/deployment/environment-variables">
    Required env vars for the coordinator, gateway, chat-relayer, and explorer-indexer
  </Card>

  <Card title="Full E2E smoke test" icon="flask" href="/guides/smoke-test">
    Reproduce a real settled inference, with troubleshooting for common failures
  </Card>
</CardGroup>
