Skip to content

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

@amrilsyaifa

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

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions