Skip to content

[Bug]: 401 Unauthorized on all browser API calls when running via Docker Compose #4012

Description

@amrilsyaifa

Before you submit

  • I searched existing issues and confirmed this is not a duplicate.
  • I replaced the example text with the real behavior, reproduction steps, expected result, and version from the app's About menu or od --version.

What happened?

Issue

When running Open Design via docker compose up -d, the browser UI loads correctly
but every API call (e.g. /api/version, /api/runs) returns 401 API_TOKEN_REQUIRED,
making the app completely unusable.

Secondary issue: docker compose build fails

scripts/postinstall.mjs includes apps/daemon in its build targets and runs
during pnpm install. At that point in the Dockerfile, only
apps/daemon/package.json has been copied — tsconfig.json and src/ are not
yet present — so tsc -p tsconfig.json fails with:

error TS5058: The specified path does not exist: 'tsconfig.json'.
Fix: Skip build targets whose tsconfig.json is absent (the Dockerfile's
explicit pnpm --filter @open-design/daemon build step after COPY apps handles
the daemon build correctly).

Environment
Docker Desktop (macOS / Windows / Linux)
docker.io/vanjayak/open-design:latest pre-built image or local build
Any browser on the Docker host

Steps to reproduce

Steps to reproduce

  1. Follow the Docker quickstart in deploy/README.md
  2. Set OD_API_TOKEN in deploy/.env
  3. docker compose up -d
  4. Open http://localhost:7456 in a browser
  5. Open DevTools → Network tab

Result: Every /api/* request returns:

{
  "error": {
    "code": "API_TOKEN_REQUIRED",
    "message": "Authorization: Bearer <OD_API_TOKEN> required"
  }
}

Expected behavior

Expected behavior
The browser UI should work without manually sending a Bearer token —
same as the local dev (pnpm tools-dev) experience.

Open Design version

0.8.1

Platform

macOS (Apple Silicon)

Logs (optional)

Screenshots (optional)

Issue

Screen.Recording.2026-06-09.at.22.14.08.mov

Expected

Screen.Recording.2026-06-09.at.22.16.47.mov

Additional context

Root cause
Docker NAT. When Docker forwards 127.0.0.1:7456 (host) → container port 7456,
the daemon sees the TCP peer address as the Docker bridge gateway (e.g. 172.17.0.1),
not 127.0.0.1.

The auth middleware in apps/daemon/src/server.ts bypasses the Bearer check for
loopback peer addresses:

if (isLoopbackPeerAddress(req.socket?.remoteAddress)) return next();
isLoopbackPeerAddress('172.17.0.1') returns false, so the bypass never fires.
The web frontend never sends a Bearer token (it has always relied on this bypass),
so every API call returns 401.

Affected request headers (from browser DevTools)

host: 127.0.0.1:7456
sec-fetch-site: same-origin
// no origin header
// no authorization header
Fix
Add isLoopbackBrowserRequest() as a fallback — checks the HTTP Host and
Origin headers instead of the TCP peer address. Both headers are browser-set
and survive Docker NAT intact.

function isLoopbackBrowserRequest(req): boolean {
  const hostname = (req.get('host') ?? '').replace(/:\d+$/, '');
  if (!isLoopbackHostname(hostname)) return false;
  const origin = req.get('origin');
  if (!origin) return true;
  try { return isLoopbackHostname(new URL(origin).hostname); }
  catch { return false; }
}

Auth middleware bypass updated to:

if (isLoopbackPeerAddress(req.socket?.remoteAddress) || isLoopbackBrowserRequest(req)) return next();

Security: Origin must also be loopback when present — blocks DNS-rebinding attacks
and prevents OD_ALLOWED_ORIGINS reverse-proxy deployments from accidentally
bypassing Bearer auth.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions