Skip to content

fix(app): tell the user localhost means the container, not their machine (#1346) - #1347

Merged
alfredo1996 merged 5 commits into
release/1.4from
fix/issue-1346-docker-localhost
Jul 28, 2026
Merged

fix(app): tell the user localhost means the container, not their machine (#1346)#1347
alfredo1996 merged 5 commits into
release/1.4from
fix/issue-1346-docker-localhost

Conversation

@alfredo1996

@alfredo1996 alfredo1996 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

What

The most common thing a user does after neoboard demo is connect their own database. On a Docker install that database is on the host, so they type neo4j://localhost:7688 — and inside the app container localhost is the container.

The driver says Could not perform discovery. No routing servers available. That classified as network, whose hint told them to "verify the host and port, confirm the database is running, and check that no firewall is blocking the connection."

All four are already correct. The host is right, the port is right, the database is running and reachable, no firewall involved. Nothing in the message, the docs, or the form mentions Docker. There is no thread to pull.

How

A new container_loopback code, when the error is a network failure and the URI host is loopback and the app is containerised (/.dockerenv).

All three conditions matter:

  • Containerised. In local mode the app runs on the host, where localhost is exactly right — telling that user to use a Docker hostname sends them somewhere that does not exist for them. That regression test is the important one.
  • Network only. It narrows network, never overriding auth_failed or bad_uri. A loopback auth failure is still an auth failure.
  • Parsed, not substring-matched. myhost-localhost.example.com contains "localhost" and is not loopback. An unparseable URI returns false rather than throwing — this runs on an error path, where a throw replaces a bad message with a 500.

The half that would otherwise have made this a new bug

host.docker.internal resolves automatically on Docker Desktop but not on Linux without an explicit host-gateway mapping — and no compose file had one:

$ grep -n "extra_hosts|host-gateway" docker/*.yml
(no matches)

A hint naming a hostname that does not resolve is worse than no hint. Added to all three compose files that run the app, with an assertion so its absence can't go unnoticed — proven by deleting it from one file and watching the check name that file.

Security

The URI reaches the classifier and goes no further; it can carry a password. A test pins that neither the credentials nor the host appear in the returned result.

Verified against the running stack, before writing any of this

from inside neoboard-app:
  localhost:7688             -> FAIL ECONNREFUSED
  host.docker.internal:7688  -> OPEN, 6053 nodes

3203 app tests, typecheck, lint clean.

Closes #1346

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an opt-in --expose-host flag to the start command to map host.docker.internal and support connecting to services on the same machine from within containers.
  • Bug Fixes

    • Improved connection-test error hints for containerized setups that mistakenly use localhost/loopback addresses.
    • Enhanced error classification context and ensured user-facing errors avoid revealing sensitive connection details.
  • Tests

    • Added/expanded automated coverage for container detection, connection error classification, and Docker Compose networking overlays.

The most common thing a user does after `neoboard demo` is connect their own
database. On a Docker install that database is on the HOST, so they type
neo4j://localhost:7688 — and inside the app container localhost is the
container. Neo4j says "Could not perform discovery. No routing servers
available", which classified as `network`, whose hint told them to verify the
host, the port, that the database is running, and their firewall.

All four are already correct. The message, the docs and the form never
mention Docker, so there was no thread to pull.

Now classified as container_loopback when the error is a network failure AND
the URI host is loopback AND the app is containerised. The hint names
host.docker.internal and the service-name alternative.

All three conditions matter. In LOCAL mode the app runs on the host, where
localhost is exactly right — telling that user to use a Docker hostname sends
them somewhere that does not exist for them. And it narrows `network` only,
never overriding auth or bad_uri: a loopback auth failure is still an auth
failure.

The host is parsed, not substring-matched. "myhost-localhost.example.com"
contains "localhost" and is not loopback. An unparseable URI returns false
rather than throwing — this runs on an error path, where a throw would
replace a bad message with a 500.

Second half, and the reason the hint would otherwise be a new bug:
host.docker.internal resolves automatically on Docker Desktop but NOT on
Linux without an explicit host-gateway mapping, and no compose file had one.
A hint naming a hostname that does not resolve is worse than no hint. Added
to all three compose files that run the app, with an assertion so its absence
cannot go unnoticed — proven by deleting one and watching the check name it.

The URI reaches the classifier and goes no further: it can carry a password,
so a test pins that neither it nor the credentials appear in the result.

Verified against the running stack before writing any of this:
  from inside neoboard-app, localhost:7688 -> ECONNREFUSED
                  host.docker.internal:7688 -> OPEN, 6053 nodes

Verified: 3203 app tests, typecheck, lint.

Closes #1346

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alfredo1996 alfredo1996 added this to the Launch Readiness (GTM) milestone Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@alfredo1996, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 31621892-e2a0-407e-813c-c9f9dd82aecc

📥 Commits

Reviewing files that changed from the base of the PR and between d617837 and e1be17c.

📒 Files selected for processing (6)
  • app/src/lib/__tests__/connector/container-host.test.ts
  • app/src/lib/connector/container-host.ts
  • app/src/lib/query/query-executor.ts
  • cli/src/__tests__/commands/start.test.ts
  • cli/src/commands/start.ts
  • scripts/__tests__/docs-accuracy.test.mjs

Walkthrough

Connection tests now classify containerized loopback failures separately and provide Docker-specific guidance. The CLI adds an opt-in --expose-host overlay, with tests covering classification, propagation, sanitization, runtime detection, Compose wiring, and host-gateway configuration.

Changes

Container loopback classification

Layer / File(s) Summary
Loopback error classification
app/src/lib/connector/connection-error-classifier.ts, app/src/lib/connector/is-containerised.ts
Adds container_loopback, loopback URI detection, container context, Docker guidance, and cached runtime detection.
Connection test context propagation
app/src/lib/connector/connection-test-result.ts, app/src/app/api/connections/.../test/route.ts
Passes URI and container status from both connection-test routes into classification while preserving sanitized responses.
Docker host overlay wiring
cli/src/index.ts, cli/src/commands/start.ts, cli/src/lib/docker.ts
Adds --expose-host and conditionally includes the Docker Compose host overlay.
Validation coverage
app/src/lib/connector/__tests__/*, cli/src/__tests__/*, scripts/__tests__/docs-accuracy.test.mjs
Tests classification, precedence, sanitization, detection caching, CLI forwarding, overlay ordering, and host-gateway configuration.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ConnectionTestRoute
  participant isContainerised
  participant connectionTestErrorResult
  participant classifyConnectionError
  ConnectionTestRoute->>isContainerised: detect runtime
  ConnectionTestRoute->>connectionTestErrorResult: pass error, URI, and container status
  connectionTestErrorResult->>classifyConnectionError: classify driver message with context
  classifyConnectionError-->>connectionTestErrorResult: return container_loopback or existing code
  connectionTestErrorResult-->>ConnectionTestRoute: return sanitized result
Loading

Possibly related PRs

Suggested labels: pkg:app, testing

Suggested reviewers: alfredorubin96

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive Core loopback detection and CLI plumbing are present, but the required connections-docs update is unverified and docker/docker-compose.expose-host.yml is excluded by !docker/**. Add the connections-page documentation and surface docker/docker-compose.expose-host.yml in review, or move it out of the excluded path filter, so host-gateway support can be verified.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: Docker loopback failures now tell users localhost is inside the container.
Out of Scope Changes check ✅ Passed The changes stay focused on Docker loopback detection, CLI plumbing, compose overlay support, and tests; no unrelated refactors are evident.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1346-docker-localhost

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/__tests__/docs-accuracy.test.mjs`:
- Around line 233-237: Update the missing-file validation around the withApp
filter to parse each Compose file as YAML and inspect the app service’s
extra_hosts configuration. Require that the app service contains the
host.docker.internal:host-gateway mapping, rather than accepting matches in
comments or unrelated sections; preserve the existing missing-file behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1741e9c1-af16-444b-8d4e-2a6b08cb8831

📥 Commits

Reviewing files that changed from the base of the PR and between 6a1bdc4 and e3e6c2a.

⛔ Files ignored due to path filters (3)
  • docker/docker-compose.full.yml is excluded by !docker/**
  • docker/docker-compose.prod-full.yml is excluded by !docker/**
  • docker/docker-compose.prod.yml is excluded by !docker/**
📒 Files selected for processing (9)
  • app/src/app/api/connections/[id]/test/route.ts
  • app/src/app/api/connections/test-inline/route.ts
  • app/src/lib/connector/__tests__/connection-error-classifier.test.ts
  • app/src/lib/connector/__tests__/connection-test-result.test.ts
  • app/src/lib/connector/__tests__/is-containerised.test.ts
  • app/src/lib/connector/connection-error-classifier.ts
  • app/src/lib/connector/connection-test-result.ts
  • app/src/lib/connector/is-containerised.ts
  • scripts/__tests__/docs-accuracy.test.mjs

Comment thread scripts/__tests__/docs-accuracy.test.mjs Outdated
…e localhost

Two corrections to the first cut of this fix.

1. Host access is now a CLI flag, not a hardcoded compose line.

`neoboard start --full --expose-host` layers docker-compose.expose-host.yml,
which is the only file carrying the host-gateway mapping. Off by default:
most installs never need it — a database in the same compose network is
reached by its service name, a remote one by its hostname — and it routes
from the container out to the host's network, which is not something to
enable for everyone.

The overlay is layered AFTER the base file; compose merges left to right, so
an overlay listed first would be silently overridden and do nothing. Tested.
A second test asserts the overlay is the ONLY carrier of the mapping — if a
base file also had it, the flag would appear to work while doing nothing, and
deleting the overlay would break nothing visible until a Linux user hit it.

2. The hint said "not your machine", which is wrong on a deployed instance.

The connection is opened by the NeoBoard SERVER, not the browser. A user on
their own PC connecting to a NeoBoard deployed on a server means the SERVER's
localhost — and host.docker.internal is the server's host too, not theirs.
The old copy read as though their laptop were reachable. It is not, and there
is no flag that makes it so.

The hint now distinguishes three cases: the database on the app's own host
(--expose-host + host.docker.internal), in the same Docker network (service
name), and on the user's own computer with NeoBoard deployed elsewhere — not
reachable at all, expose it at a routable address first. That third case had
no answer before and would have sent a remote user chasing a hostname forever.

Verified against the running stack with the flag active:
  neo4j://host.docker.internal:7688   OK
  bolt://host.docker.internal:7688    OK
  neo4j://localhost:7688              container_loopback (hint fires)
  neo4j://neo4j:7687                  OK (service name still works)
And without the flag: compose resolves 0 host-gateway entries, 1 with it.

Verified: 3203 app tests, 338 CLI tests, typecheck, lint.

Refs #1346

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/src/__tests__/commands/start.test.ts`:
- Around line 277-284: Move the start command tests from
cli/src/__tests__/commands/start.test.ts to
cli/src/commands/__tests__/start.test.ts, preserving their coverage and updating
imports as needed. Move the Docker tests from
cli/src/__tests__/lib/docker.test.ts to cli/src/lib/__tests__/docker.test.ts,
likewise preserving behavior and correcting relative imports.

In `@cli/src/commands/start.ts`:
- Line 46: Validate the exposeHost option in the start command so --expose-host
is rejected unless Docker full-stack mode (--full) is enabled. Cover both
invalid combinations—local mode with --expose-host and --expose-host without
--full—while preserving valid full-stack behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fdd5ae75-3f10-48a1-8909-827ff049de01

📥 Commits

Reviewing files that changed from the base of the PR and between e3e6c2a and d617837.

⛔ Files ignored due to path filters (1)
  • docker/docker-compose.expose-host.yml is excluded by !docker/**
📒 Files selected for processing (8)
  • app/src/lib/connector/__tests__/connection-error-classifier.test.ts
  • app/src/lib/connector/connection-error-classifier.ts
  • cli/src/__tests__/commands/start.test.ts
  • cli/src/__tests__/lib/docker.test.ts
  • cli/src/commands/start.ts
  • cli/src/index.ts
  • cli/src/lib/docker.ts
  • scripts/__tests__/docs-accuracy.test.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/lib/connector/tests/connection-error-classifier.test.ts
  • app/src/lib/connector/connection-error-classifier.ts

Comment on lines +277 to +284
it("passes --expose-host through to compose (#1346)", () => {
// Off by default above; on only when asked for.
return runStart({ full: true, exposeHost: true }).then(() => {
expect(mockComposeUp).toHaveBeenCalledWith({
full: true,
exposeHost: true,
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Co-locate these tests with their sources.

  • cli/src/__tests__/commands/start.test.ts#L277-L284: move to cli/src/commands/__tests__/start.test.ts.
  • cli/src/__tests__/lib/docker.test.ts#L99-L134: move to cli/src/lib/__tests__/docker.test.ts.

As per coding guidelines, tests live in an __tests__/ directory next to the file under test, within the same package.

📍 Affects 2 files
  • cli/src/__tests__/commands/start.test.ts#L277-L284 (this comment)
  • cli/src/__tests__/lib/docker.test.ts#L99-L134
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/__tests__/commands/start.test.ts` around lines 277 - 284, Move the
start command tests from cli/src/__tests__/commands/start.test.ts to
cli/src/commands/__tests__/start.test.ts, preserving their coverage and updating
imports as needed. Move the Docker tests from
cli/src/__tests__/lib/docker.test.ts to cli/src/lib/__tests__/docker.test.ts,
likewise preserving behavior and correcting relative imports.

Source: Coding guidelines

Comment thread cli/src/commands/start.ts
… properly

Two CodeRabbit findings, both valid.

The Major one is a real bug I shipped. --expose-host overlays extra_hosts onto
the `neoboard` service, which only the FULL compose defines. Without --full the
overlay lands on a service that does not exist and compose refuses the entire
project:

  service "neoboard" has neither an image nor a build context specified:
  invalid compose project

An error about the wrong thing entirely — nothing in it says "you forgot
--full". Verified against real `docker compose config` before and after. Local
mode is rejected too, with a different reason: the app runs on the host there,
so localhost already reaches the databases and the flag has nothing to do.

The Minor one: the compose assertion was a raw substring check, which would
also pass from a comment, an unrelated service, or malformed YAML — and a
mapping on the WRONG service is precisely the failure it guards against. Now
resolves the project with `docker compose config --format json` and asserts
extra_hosts on the app service specifically.

Declined, with reason: "co-locate these tests with their sources". CLAUDE.md
does say tests live next to the file under test, but the cli/ package
centralises every test under cli/src/__tests__/ — all 30 files. Moving one
would make that package inconsistent rather than compliant. The convention gap
is real and worth settling repo-wide; doing it inside this PR would bury a
networking fix under a test migration.

Verified: 341 CLI tests, 42 script tests, lint. And the real binary:
`neoboard start --expose-host` now prints the fix instead of a compose error.

Refs #1346

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alfredo1996

Copy link
Copy Markdown
Owner Author

All three addressed.

Major — --expose-host needs Docker full-stack mode. Real bug I shipped, and worth showing what it actually did. The overlay adds extra_hosts to the neoboard service, which only the full compose defines. Without --full, compose refuses the entire project:

service "neoboard" has neither an image nor a build context specified: invalid compose project

An error about the wrong thing entirely — nothing in it says "you forgot --full". Verified against real docker compose config both before and after. Local mode is rejected too, with a different message: the app runs on the host there, so localhost already reaches the databases and the flag has nothing to do. Three tests, plus the real binary now printing the fix instead of a compose error.

Minor — assert on the app service, not anywhere in the file. Correct: the raw substring would also pass from a comment, an unrelated service, or malformed YAML, and a mapping on the wrong service is exactly the failure that check exists to catch. Now resolves the project with docker compose config --format json and asserts extra_hosts on the app service specifically.

Minor — co-locate tests with sources: declining, with reason. CLAUDE.md does say tests live next to the file under test, but the cli/ package centralises all 30 test files under cli/src/__tests__/. Moving one would make that package inconsistent rather than compliant. The convention gap is real and worth settling repo-wide — doing it inside this PR would bury a networking fix under a test migration. Happy to file it separately if you'd like.

341 CLI tests, 42 script tests, lint clean.

alfredorubin96 and others added 2 commits July 28, 2026 17:39
CI failed on the check I had just made more rigorous. It passed `--env-file
docker/.env`, which is gitignored and CLI-generated: present on my machine,
absent in CI.

  couldn't find env file: /home/runner/work/neoboard/neoboard/docker/.env

This is the same trap as #1221, in the test written to avoid traps. There I
found that a `${VAR:?}` guard appeared not to work because docker/.env
silently supplied the value; here the same file silently made a check pass.
A test that reads a generated file is testing the machine it runs on.

Now supplies the compose file's three required vars as placeholders in the
child process env, so the check is self-contained. Verified the way it should
have been the first time: moved docker/.env aside, ran it, watched it pass.

Refs #1346

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Third failure on this one check, and the most avoidable. Compose normalises
extra_hosts differently across versions: CI emits

  host.docker.internal=host-gateway

where this machine emits `:`. A literal comparison against my own compose's
output is a check that asserts my Docker version.

Now normalises the separator and accepts the map shape too, so it compares
what the entry MEANS. Verified against all three forms — array with `:`,
array with `=`, host->target object — plus a negative case, rather than
against whatever this machine happens to print.

Worth noting what the three failures actually were, because none was noise:
the substring check would have passed from a comment; the --env-file
dependency made it pass only where a gitignored file existed; and the literal
comparison pinned one compose version. Each iteration asserted something
narrower than the thing I meant.

Refs #1346

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@alfredo1996
alfredo1996 merged commit 880a76d into release/1.4 Jul 28, 2026
17 checks passed
@alfredo1996
alfredo1996 deleted the fix/issue-1346-docker-localhost branch July 28, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants