# Install nymph

`nymph` is the Lakeshore worker daemon: a single static Rust binary that
long-polls the control plane, runs the work it is handed, and reports
back. It needs no inbound port — everything is outbound HTTPS.

Once installed, running the bare binary (no subcommand) *is* the daemon.
The only subcommand is [`nymph tui`](/nymph/status-tui.md).

## One-liner

```bash
curl -fsSL https://dreamlake.ai/dreamlake/nymph/install.sh | bash
```

`scripts/install.sh` does exactly four things:

1. Maps `uname -s` → `unknown-linux-gnu` / `apple-darwin` and `uname -m`
   → `x86_64` / `aarch64`. Any other OS or arch is a hard error.
2. Downloads `${BASE_URL}/${VERSION}/nymph-${arch}-${os}.tar.gz` and its
   `.sha256` sidecar.
3. Verifies the checksum with `sha256sum -c` (or `shasum -a 256 -c` on
   macOS).
4. Untars and moves `nymph` into `/usr/local/bin`, escalating with
   `sudo` only when that directory is not writable.

| Flag / env       | Default                                                            | Meaning                             |
| ---------------- | ------------------------------------------------------------------ | ----------------------------------- |
| `--version <v>`  | `latest`                                                           | Release directory to pull from.     |
| `--dest <dir>`   | `/usr/local/bin`                                                   | Install directory.                  |
| `NYMPH_BASE_URL` | `https://pub-c0109e197b4a4d1abe5884ac4dd3a023.r2.dev/dreamlake/nymph` | Release bucket base URL. |

```bash
curl -fsSL https://dreamlake.ai/dreamlake/nymph/install.sh | bash -s -- --version 0.1.4
curl -fsSL https://dreamlake.ai/dreamlake/nymph/install.sh | bash -s -- --dest ~/.local/bin
```

> **Warning:** The install script installs a binary and nothing else — there is no
> `--service` flag and no `NYMPH_PREFIX` variable. Running nymph under a
> service manager is covered in [Run it as a service](#run-it-as-a-service)
> below.

### Published targets

CI builds on a `v*` tag push for exactly three targets:

- `x86_64-unknown-linux-gnu`
- `aarch64-unknown-linux-gnu`
- `aarch64-apple-darwin`

Intel macOS (`x86_64-apple-darwin`) was dropped from CI. Each target
ships a `nymph-<target>.tar.gz` (what `install.sh` consumes) *and* a raw
`nymph-<target>` binary (what the OTA path consumes), each with a
`.sha256` sidecar, under `dreamlake/nymph/<version>/` in the release
bucket.

## Build from source

```bash
git clone https://github.com/dreamlake-ai/nymph.git
cd nymph
cargo build --release      # target/release/nymph
```

The crate is `publish = false` — it is never on crates.io. The release
profile is size-tuned (`opt-level = "z"`, fat LTO, symbols stripped,
`panic = "abort"`).

The `ota-self-replace` feature is **on by default**: after a successful
[OTA update](/nymph/protocol.md#ota_update) the daemon `exec()`s into the
freshly installed binary. Build with `--no-default-features` to install
the new binary but keep the old process running until you restart it
yourself.

## Point it at a control plane

The daemon reads one TOML file at boot and never re-reads it. With no
`--config`, it tries these in order:

1. `/etc/dreamlake/udf-daemon.toml`
2. `$HOME/.config/dreamlake/udf-daemon.toml`
3. built-in defaults (`http://localhost:8080`, `process` runner)

A minimal working config:

```toml file="/etc/dreamlake/udf-daemon.toml"
[server]
url = "https://api.lakeshore.dreamlake.ai"
token_file = "/etc/dreamlake/token"
namespace = "default"

[daemon]
label = "bos14-login"
tags = ["gpu:h100"]

[runtime]
workdir = "/var/lib/dreamlake/udf"
runners = ["process"]
default_runner = "process"
keep_alive_s = -1
```

> **Warning:** The top-level config struct is `#[serde(deny_unknown_fields)]`. A stray
> section — `[relay]`, or a bare top-level key like `server_url = "…"` —
> fails the parse and the daemon refuses to boot. The nine valid sections
> are `[server]`, `[daemon]`, `[runtime]`, `[poll]`, `[log]`,
> `[shutdown]`, `[mounts]`, `[introspect]`, `[identity]`. Unknown keys
> *inside* a valid section are silently ignored.

Then start it:

```bash
nymph                                    # uses the default config search path
nymph --config /etc/dreamlake/udf-daemon.toml
```

### Daemon flags and env vars

| Flag              | Env var             | Default     | Meaning                                          |
| ----------------- | ------------------- | ----------- | ------------------------------------------------ |
| `--config <path>` | `LAKESHORED_CONFIG` | search path | Path to `udf-daemon.toml`.                        |
| `--server <url>`  | `LAKESHORE_SERVER`  | from config | Control-plane base URL.                           |
| `--token <tok>`   | `LAKESHORE_TOKEN`   | from config | Bootstrap bearer used on `/hello`.                |
| `--queue <name>`  | `DREAMLAKE_QUEUE`   | `default`   | Queue to subscribe to. A trailing `*` is a prefix match. |
| `--worker-id <id>`| —                   | derived     | Override the provisional `{machine_id}-{ULID}` id. |

Two more env vars affect logging only: `RUST_LOG` (full `EnvFilter`
syntax, wins over `[log] level`) and `LAKESHORE_LOG_FORMAT` (`human` or
`json`, wins over `[log] format`). See
[Telemetry and introspection](/nymph/telemetry.md).

> **Note:** It is `LAKESHORED_CONFIG`, not `LAKESHORE_CONFIG`. The queue override is
> `DREAMLAKE_QUEUE`, not `LAKESHORE_QUEUE`.

## Verify it registered

```bash
lakeshore daemon list
```

The daemon posts `/v1/daemon/hello` on boot, then long-polls. It shows
up as `joining` on the hello and flips to `active` on its first poll.
See [Daemon lifecycle](/nymph/daemon-lifecycle.md) for the full operator
surface.

## Run it as a service

`install.sh` does not write a unit file. Two supported paths:

- **Control-plane launch.** `lakeshore daemon launch` renders a
  cloud-init script that installs the binary at `/opt/dreamlake/bin/nymph`,
  writes `/etc/dreamlake/udf-daemon.toml`, and installs + enables a
  `dreamlake-nymph.service` systemd unit (`Restart=always`,
  `KillMode=mixed`, `TimeoutStopSec=30`, logs appended to
  `/var/log/dreamlake/nymph.log`). See
  [Daemon lifecycle → launch](/nymph/daemon-lifecycle.md#launch-through-a-provider).
- **SSH bootstrap.** `lakeshore daemon install <alias>` starts the
  daemon under `setsid nohup` (or `sbatch` with `--slurm`) in the SSH
  user's `$HOME` — no systemd, no sudo. See
  [Bootstrap a daemon on a remote host](/nymph/install-remote-daemon.md).

For a hand-rolled unit, mirror the launch path: run as a non-root user
that owns the binary and the workdir, `KillMode=mixed` so `SIGTERM`
reaches the daemon directly (it does its own
[graceful drain](/nymph/daemons.md#shutdown-and-drain)), and a
`TimeoutStopSec` comfortably above `[shutdown] grace_s`.

## Where to next

| I want to…                                   | Page                                                              |
| -------------------------------------------- | ----------------------------------------------------------------- |
| Bootstrap nymph on a host over SSH            | [Bootstrap a remote daemon](/nymph/install-remote-daemon.md)         |
| Understand the daemon's runtime and runners   | [Daemons](/nymph/daemons.md)                                          |
| Manage daemons from the CLI                   | [Daemon lifecycle](/nymph/daemon-lifecycle.md)                        |
| Read the wire protocol                        | [Daemon protocol](/nymph/protocol.md)                                 |
| Turn on keypair auth                          | [Daemon identity and key rotation](/nymph/key-rotation.md)            |
| Watch a local daemon live                     | [`nymph tui`](/nymph/status-tui.md)                                   |
| Push a debug build without a release          | [Nymph push (dev)](/dev/nymph-push)                                |
