Replace the assumed/unconfirmed environment notes with facts verified from this box, and add docs/odin-access.md as the access reference. Verified: - Tailscale is the only path from dev to odin. dev (100.94.16.46) is a VPS with a public IP, not a LAN machine; odin is 100.101.253.105, and a subnet route puts 192.168.90.0/24 over tailscale0 at the same ~34ms RTT. - The myunraid.net host is the canonical nginx vhost, not a cloud relay -- DNS resolves it to the private 192.168.90.103, so traffic stays on the tailnet. The bare IP 404s on /graphql, and plain HTTP 302-redirects to the myunraid host while stripping the x-api-key header. - GraphQL is live: unauthenticated POSTs return HTTP 200 with an UNAUTHENTICATED error body, so 200 must never be read as success. Corrects unraid-docker-manager's claim that no SSL ignore is needed: TLS verification fails from dev because ca-certificates 20250419 cannot chain Let's Encrypt intermediate YR1. The cert is genuine and the claim likely holds on odin itself, but curl needs -k here. Container-polling field behaviours (UPPERCASE state, /-prefixed names, PrefixedID ids, no isUpdateAvailable in 7.2) are carried over from unraid-docker-manager, which is credited as the authoritative source. No API key is present on this box; .env.unraid-api is gitignored there and absent from the clone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.8 KiB
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://<myunraid-host>: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, whereUNRAID_HOSTis themyunraid.netbase URL without the/graphqlsuffix. - Auth:
x-api-key: <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:
{"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
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 } } }"}'
{"data":{"docker":{"containers":[
{"id":"<server_hash>:<container_hash>","names":["/n8n"],"state":"RUNNING"}
]}}}
Field behaviours that bite:
stateis UPPERCASE —RUNNING, notrunning.namesis an array and entries are/-prefixed —/n8n, notn8n.idis aPrefixedID:{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.isUpdateAvailabledoes 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:
unraid-api apikey --create --name "<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 separatesourcecall is lost. - Large API responses: save to a temp file before parsing; piping a 400 KB body
straight into
python3 -cfails silently. - Docker API
204 No Contentmeans success with an empty body.