# Accessing odin Reference for reaching the Unraid host (**odin**) from this workstation (`dev`) and driving its API. Everything here was verified from `dev` on 2026-08-15; the "Verified" column says how. Related repo: **`luckberg/unraid-docker-manager`** on `git.bergerhouse.net` — an n8n Telegram bot that manages odin's containers. It is the origin of most of the API detail below and is worth reading before writing anything new against the Unraid API. --- ## Network path: Tailscale `dev` and odin are both on the tailnet. `dev` is a **VPS** (public IP `51.222.234.162`), *not* a machine on the home LAN — Tailscale is the only path to odin. | Node | Tailscale IP | Notes | | --- | --- | --- | | `dev` | `100.94.16.46` | this workstation (linux) | | `odin` | `100.101.253.105` | the Unraid host (linux) | Other tailnet nodes seen: `duplicati`, `experience` (windows), `fedora`, `nusgwvm`, `salt`, `google-pixel-8-pro`. **A subnet route is advertised for the home LAN.** odin's LAN address `192.168.90.103` is reachable from `dev` and routes over `tailscale0`: ``` $ ip route get 192.168.90.103 192.168.90.103 dev tailscale0 table 52 src 100.94.16.46 ``` Both the tailnet IP and the LAN IP answer at ~34 ms RTT — the same path. This matters: the `myunraid.net` URL below looks like a cloud relay, but **DNS resolves it to the private `192.168.90.103`**, so traffic goes direct over Tailscale and never leaves the tailnet. If Tailscale is down, none of it is reachable. --- ## WebGUI / API endpoint ``` https://192-168-90-103.87c90a69c53e7197560c778a63f483e675130f2a.myunraid.net:8443/ ``` The hostname encodes odin's LAN IP with dashes, under a per-server hash on `myunraid.net`. This is the **canonical vhost** — not merely a convenience alias. Use it for everything. ### Why the plain LAN IP does not work Both failure modes are real and confirmed: | Attempt | Result | Verified | | --- | --- | --- | | `https://192.168.90.103:8443/graphql` | **404** from nginx | curl | | `http://192.168.90.103/graphql` | **302** → the `myunraid.net` URL | curl | | `https://:8443/graphql` | **200**, GraphQL responds | curl | nginx only serves `/graphql` on the `myunraid.net` `server_name`; the raw IP vhost returns 404. Plain HTTP redirects to that hostname — and **the redirect strips the `x-api-key` header**, so a client that follows redirects authenticates as nobody. Always call the `myunraid.net` URL directly rather than relying on a redirect. ### TLS caveat — correcting the other repo `unraid-docker-manager` states "Valid certs via myunraid.net — no SSL ignore needed." **That is not true from `dev`.** Verification fails here: ``` depth=1 C=US, O=Let's Encrypt, CN=YR1 verify error:num=20:unable to get local issuer certificate depth=0 CN=*.87c90a69c53e7197560c778a63f483e675130f2a.myunraid.net ``` The leaf is a genuine Let's Encrypt wildcard, but the chain presents an intermediate (`YR1`) that this box's CA bundle (`ca-certificates 20250419`) cannot chain to a trusted root — either the server omits the intermediate or the bundle predates it. The cert is not forged; the chain is incomplete *for this client*. Practical consequence: **`curl` needs `-k` from `dev`**, and any script that talks to odin must either pass the insecure flag or pin/supply the issuer. n8n running *on* odin may well verify fine, which is likely why the other repo's claim held there. Prefer supplying the missing intermediate over making `-k` a permanent habit — `-k` disables verification entirely, which on a tailnet is a tolerable but real weakening. --- ## Unraid GraphQL API - **Endpoint:** `{UNRAID_HOST}/graphql`, POST, where `UNRAID_HOST` is the `myunraid.net` base URL **without** the `/graphql` suffix. - **Auth:** `x-api-key: ` header. - **Availability:** native in Unraid 7.2+ (odin is on 7.2); 6.9–7.1 needs the Connect plugin. Unauthenticated requests return HTTP **200** with a GraphQL error body — not an HTTP 401: ```json {"errors":[{"message":"Invalid CSRF token","extensions":{"code":"UNAUTHENTICATED", "originalError":{"error":"Unauthorized","statusCode":401}}}],"data":null} ``` **Never treat HTTP 200 as success.** Always inspect `response.errors[]`. ### Polling running containers ```bash curl -sS -k -X POST "${UNRAID_HOST}/graphql" \ -H 'Content-Type: application/json' \ -H "x-api-key: ${UNRAID_API_KEY}" \ -d '{"query":"query { docker { containers { id names state } } }"}' ``` ```json {"data":{"docker":{"containers":[ {"id":":","names":["/n8n"],"state":"RUNNING"} ]}}} ``` Field behaviours that bite: - `state` is **UPPERCASE** — `RUNNING`, not `running`. - `names` is an array and entries are **`/`-prefixed** — `/n8n`, not `n8n`. - `id` is a `PrefixedID`: `{server_hash}:{container_hash}`, two 64-char SHA-256 hex strings joined by a colon. The server half is identical for every container on odin. - **`isUpdateAvailable` does not exist** in the 7.2 schema, despite appearing in that repo's earlier research. Introspect before relying on any field. ### API keys Not present on this box — `.env.unraid-api` is gitignored and absent from the clone. Create a key scoped to what the script actually needs: ```bash unraid-api apikey --create --name "" --permissions "DOCKER:UPDATE_ANY" --json ``` Or WebGUI → Settings → Management Access → API Keys. Store it in the collection's own `.env`, never in git. --- ## Gotchas carried over from `unraid-docker-manager` - **Each Bash tool call is a fresh shell.** Source the env file in the *same* command chain as the request: `. .env.unraid-api; curl ...`. A separate `source` call is lost. - **Large API responses:** save to a temp file before parsing; piping a 400 KB body straight into `python3 -c` fails silently. - Docker API `204 No Content` means success with an empty body.