docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# =============================================================================
|
|
|
|
|
# Headroom — full "memory stack" compose
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Brings up the Headroom proxy together with the two datastores it needs for
|
|
|
|
|
# semantic memory: Qdrant (vector search) and Neo4j (relationship graph).
|
|
|
|
|
#
|
|
|
|
|
# Quick start:
|
|
|
|
|
# 1. cp .env.example .env # then set a real NEO4J_AUTH before any non-local use
|
|
|
|
|
# 2. docker compose up -d
|
|
|
|
|
# 3. point your LLM client at http://localhost:8787 (proxy)
|
|
|
|
|
#
|
|
|
|
|
# Just want the proxy without the memory features? You can run the proxy image
|
fix(docker): ship Bedrock auth and current registry (#2982)
## Description
Fixes #1551 and #1692.
Every published Headroom Docker image now installs the existing
`bedrock` extra, so `--backend bedrock` can authenticate with temporary
STS, SSO, and credential-process credentials instead of failing because
`botocore` is absent.
Public Docker instructions now consistently use
`ghcr.io/headroomlabs-ai/headroom`. Several still pointed at the old
personal package, which is frozen at 0.27.0 and caused users to report
that no latest image existed.
## Type of Change
- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [x] Documentation update
- [x] Build / CI
## Changes Made
- Add `bedrock` to the standalone Dockerfile default extras.
- Add `bedrock` to all nine root/code/slim/nonroot bake targets.
- Replace obsolete personal GHCR references in README, llms.txt, Compose
guidance, testing guidance, and wiki docs.
- Add release contract tests for Bedrock dependencies and the current
organization registry.
## Testing
- [x] Focused Docker release and Bedrock preflight tests pass.
- [x] Full updater suites pass: 69 tests.
- [x] `uv run ruff check tests/test_release_workflows.py`
- [x] `docker buildx bake --print`
- [x] `git diff --check`
## Real Behavior Proof
Before this change, every published bake target installed only `proxy`
or `proxy,code`, so `AWS_SESSION_TOKEN` selected an unavailable botocore
path. Public copy-paste commands also referenced
`ghcr.io/chopratejas/headroom`, which the existing migration code and
changelog identify as frozen at 0.27.0.
After this change, all nine parsed bake targets install `bedrock`; the
regression resolves that package extra and confirms `boto3` plus
`botocore`. Every public Docker instruction covered by the contract
names `ghcr.io/headroomlabs-ai/headroom`.
## Runtime Rollout Safety
This changes image contents and documentation only; proxy routing and
non-Docker installs are unchanged. Static AWS credentials remain
unaffected. Existing manifests using the deprecated image continue to be
migrated by the established install-state logic. Rollback is a
Docker/bake extras and documentation revert.
## Review Readiness
- [x] Two related Docker blockers batched in one PR
- [x] Regression coverage included
- [x] No unrelated lockfile changes
- [x] Ready for review
2026-08-13 15:06:21 -05:00
|
|
|
# on its own (`docker run -p 8787:8787 ghcr.io/headroomlabs-ai/headroom`); the two
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# database services below are only required for the memory/relevance features.
|
|
|
|
|
#
|
fix(docker): publish compose ports on loopback only (#3061)
## Description
`docker compose up -d` published every service on `0.0.0.0`, and none of
the three authenticates an inbound caller by default:
| port | service | default auth |
|---|---|---|
| 8787 | proxy | `/v1/*` data plane open unless `HEADROOM_PROXY_TOKEN`
is set |
| 6333/6334 | Qdrant | **none at all** — holds embeddings derived from
prompts |
| 7474/7687 | Neo4j | `NEO4J_AUTH` falls back to `neo4j/devpassword`,
published in this file |
So the shipped default handed any peer on the surrounding network a
relay through the proxy plus direct read/write on the vector and graph
stores built from the operator's own prompt content. The proxy already
warns about exactly this shape at `headroom/proxy/server.py:3289` — the
compose file just never took its own advice.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
## Changes Made
- Pinned all five published ports to `127.0.0.1`.
- Documented in the file header how to expose the proxy deliberately,
pairing the port override with `HEADROOM_PROXY_TOKEN` rather than
leaving that implicit.
- Added a commented `HEADROOM_PROXY_TOKEN` entry to the proxy service
environment.
- Added a regression test asserting every published port names a
loopback host IP.
## Testing
- [x] Unit tests pass
- [x] Linting passes (ruff check + format)
- [ ] Type checking passes — N/A (YAML + test only)
- [x] New tests added for new functionality
### Test Output
```text
$ .venv/bin/python -m pytest tests/test_docker_compose_persistence.py -q
3 passed in 0.14s
$ docker compose -f docker-compose.yml config # validates
headroom-proxy host_ip=127.0.0.1 published=8787 -> 8787
neo4j host_ip=127.0.0.1 published=7474 -> 7474
neo4j host_ip=127.0.0.1 published=7687 -> 7687
qdrant host_ip=127.0.0.1 published=6333 -> 6333
qdrant host_ip=127.0.0.1 published=6334 -> 6334
```
Against the parent commit:
```text
FAILED test_top_level_compose_publishes_only_to_loopback
E AssertionError: headroom-proxy: port '8787:8787' publishes on all interfaces
```
## Real Behavior Proof
- Environment: macOS 15 (darwin 25.4.0), Docker Compose v2 available
locally.
- Exact command / steps: `docker compose -f docker-compose.yml config
--format json` before and after, comparing the resolved `host_ip` on
every published port.
- Observed result: before, no port carried a `host_ip` (Docker binds
`0.0.0.0`); after, all five resolve to `host_ip=127.0.0.1`. The compose
file still validates.
- Not tested: bringing the stack up and probing the ports from a second
machine on the LAN — the assertion is made against Docker's own resolved
configuration rather than a live two-host network.
## Runtime Rollout Safety
- Rollout-managed feature(s): none.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: yes — the compose stack is no longer
reachable from other machines by default.
- Kill switch / disable path: override `ports:` in a
`docker-compose.override.yml`; the header documents this and pairs it
with `HEADROOM_PROXY_TOKEN`.
- Unsafe override required: none.
- Qualification impact: none.
- Rollback path: revert this commit.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation (the
compose header)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
## Additional Notes
**This is a deliberate breaking change for one workflow**: anyone
reaching the compose proxy from another machine will need to override
`ports:`. That is exactly the configuration that was unsafe, so it
should break loudly rather than silently. `http://localhost:8787` from
the host is unchanged, the container still listens on `0.0.0.0`
internally, and service-to-service traffic on the compose network is
unaffected.
Scope note: I fixed all three services rather than only the proxy.
Closing 8787 while leaving an unauthenticated Qdrant and a
default-password Neo4j published on `0.0.0.0` would not have improved
the security posture.
Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
2026-08-16 19:05:29 -07:00
|
|
|
# Ports published on the host — all bound to 127.0.0.1 (this machine only):
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# 8787 proxy (OpenAI-compatible endpoint)
|
|
|
|
|
# 6333 Qdrant REST 6334 Qdrant gRPC
|
|
|
|
|
# 7474 Neo4j Browser 7687 Neo4j Bolt
|
fix(docker): publish compose ports on loopback only (#3061)
## Description
`docker compose up -d` published every service on `0.0.0.0`, and none of
the three authenticates an inbound caller by default:
| port | service | default auth |
|---|---|---|
| 8787 | proxy | `/v1/*` data plane open unless `HEADROOM_PROXY_TOKEN`
is set |
| 6333/6334 | Qdrant | **none at all** — holds embeddings derived from
prompts |
| 7474/7687 | Neo4j | `NEO4J_AUTH` falls back to `neo4j/devpassword`,
published in this file |
So the shipped default handed any peer on the surrounding network a
relay through the proxy plus direct read/write on the vector and graph
stores built from the operator's own prompt content. The proxy already
warns about exactly this shape at `headroom/proxy/server.py:3289` — the
compose file just never took its own advice.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
## Changes Made
- Pinned all five published ports to `127.0.0.1`.
- Documented in the file header how to expose the proxy deliberately,
pairing the port override with `HEADROOM_PROXY_TOKEN` rather than
leaving that implicit.
- Added a commented `HEADROOM_PROXY_TOKEN` entry to the proxy service
environment.
- Added a regression test asserting every published port names a
loopback host IP.
## Testing
- [x] Unit tests pass
- [x] Linting passes (ruff check + format)
- [ ] Type checking passes — N/A (YAML + test only)
- [x] New tests added for new functionality
### Test Output
```text
$ .venv/bin/python -m pytest tests/test_docker_compose_persistence.py -q
3 passed in 0.14s
$ docker compose -f docker-compose.yml config # validates
headroom-proxy host_ip=127.0.0.1 published=8787 -> 8787
neo4j host_ip=127.0.0.1 published=7474 -> 7474
neo4j host_ip=127.0.0.1 published=7687 -> 7687
qdrant host_ip=127.0.0.1 published=6333 -> 6333
qdrant host_ip=127.0.0.1 published=6334 -> 6334
```
Against the parent commit:
```text
FAILED test_top_level_compose_publishes_only_to_loopback
E AssertionError: headroom-proxy: port '8787:8787' publishes on all interfaces
```
## Real Behavior Proof
- Environment: macOS 15 (darwin 25.4.0), Docker Compose v2 available
locally.
- Exact command / steps: `docker compose -f docker-compose.yml config
--format json` before and after, comparing the resolved `host_ip` on
every published port.
- Observed result: before, no port carried a `host_ip` (Docker binds
`0.0.0.0`); after, all five resolve to `host_ip=127.0.0.1`. The compose
file still validates.
- Not tested: bringing the stack up and probing the ports from a second
machine on the LAN — the assertion is made against Docker's own resolved
configuration rather than a live two-host network.
## Runtime Rollout Safety
- Rollout-managed feature(s): none.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: yes — the compose stack is no longer
reachable from other machines by default.
- Kill switch / disable path: override `ports:` in a
`docker-compose.override.yml`; the header documents this and pairs it
with `HEADROOM_PROXY_TOKEN`.
- Unsafe override required: none.
- Qualification impact: none.
- Rollback path: revert this commit.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation (the
compose header)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
## Additional Notes
**This is a deliberate breaking change for one workflow**: anyone
reaching the compose proxy from another machine will need to override
`ports:`. That is exactly the configuration that was unsafe, so it
should break loudly rather than silently. `http://localhost:8787` from
the host is unchanged, the container still listens on `0.0.0.0`
internally, and service-to-service traffic on the compose network is
unaffected.
Scope note: I fixed all three services rather than only the proxy.
Closing 8787 while leaving an unauthenticated Qdrant and a
default-password Neo4j published on `0.0.0.0` would not have improved
the security posture.
Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
2026-08-16 19:05:29 -07:00
|
|
|
#
|
|
|
|
|
# None of these three services authenticates inbound callers by default: the
|
|
|
|
|
# proxy's /v1/* data plane is open unless HEADROOM_PROXY_TOKEN is set, Qdrant
|
|
|
|
|
# has no API key, and Neo4j falls back to a published dev password. Publishing
|
|
|
|
|
# them on 0.0.0.0 therefore hands any peer on your network a relay through the
|
|
|
|
|
# proxy plus direct read/write on the embeddings and graph derived from your
|
|
|
|
|
# prompts. They are bound to loopback so that `docker compose up -d` is safe on
|
|
|
|
|
# a shared or untrusted network.
|
|
|
|
|
#
|
|
|
|
|
# To reach the proxy from another machine, publish it deliberately AND require
|
|
|
|
|
# a token — never one without the other:
|
|
|
|
|
# HEADROOM_PROXY_TOKEN=$(openssl rand -hex 32) # put this in .env
|
|
|
|
|
# ports: ["8787:8787"] # override in a compose override file
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# =============================================================================
|
|
|
|
|
|
2026-01-20 00:17:05 +01:00
|
|
|
services:
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# Headroom proxy — the OpenAI-compatible endpoint your client talks to.
|
|
|
|
|
# Built from the repo Dockerfile so it tracks your local checkout.
|
2026-01-20 00:17:05 +01:00
|
|
|
headroom-proxy:
|
fix(docker): report source build version (#1862)
## Description
Closes #1858
Docker/Compose source builds could report stale or misleading version
information: the dashboard initially rendered a hardcoded `v0.3.0`, then
`/health` replaced it with installed package metadata, which can be
stale when building locally from `main` without release metadata in the
image.
This change makes source Docker Compose builds report an explicit
source-build identity, removes the stale dashboard fallback, and keeps
CLI/doctor version checks from treating source-build labels as
release-version drift.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Add `HEADROOM_VERSION` / `HEADROOM_BUILD_VERSION` runtime version
overrides and optional packaged `_build_info.py` metadata.
- Teach Docker Compose source builds to pass a `source-build` sentinel
that the Dockerfile expands to `source-build+g<sha>` when git metadata
is available, or `source-build+sha256.<digest>` otherwise.
- Keep release/published image builds on normal package metadata when
`HEADROOM_BUILD_VERSION` is unset.
- Include only minimal `.git` metadata in the Docker build context so
the source-build label can identify the checkout without copying git
objects.
- Treat source-build labels and raw hashes as non-release labels in
`wrap` and `doctor`, avoiding false stale-proxy restarts and drift
warnings.
- Replace the dashboard hardcoded `0.3.0` fallback with `loading` /
`unknown` and format non-release build labels without a `v` prefix.
- Include the runtime version in proxy startup logs, `/health`,
`/livez`, and OTEL service version reporting.
## Testing
- [x] Unit tests pass (`pytest` in GitHub CI)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
GitHub CI: all checks passing
- CI: build, build-wheel, lint, test shards, test-extras, test-agno, test-dashboard-ui
- Docker: docker-native-e2e, docker-wrap-e2e, docker-init-e2e
- Native wrappers: macOS, Windows, Ubuntu
- Security: CodeQL, gitleaks, pip-audit
- Governance: template, label, merge-conflicts, commitlint
$ HEADROOM_REQUIRE_RUST_CORE=false PYTHONPATH=/Users/vinaygupta/Desktop/git/headroom-fix-1858-version-mismatch pytest tests/test_package_init_lazy.py::test_version_prefers_explicit_build_env tests/test_package_init_lazy.py::test_version_label_helpers_only_prefix_release_versions tests/test_package_init_lazy.py::test_version_uses_packaged_build_metadata tests/test_package_init_lazy.py::test_observability_version_uses_runtime_version tests/test_docker_compose_persistence.py tests/test_cli_doctor.py::TestProxyLiveness::test_up_leaves_source_label_unprefixed tests/test_cli_doctor.py::TestVersionDrift::test_non_release_version_labels_skip_drift_comparison tests/test_cli/test_wrap_persistent.py::test_proxy_version_restart_ignores_non_release_source_labels tests/test_proxy_dashboard_stats_cache.py::test_dashboard_uses_cached_stats_and_lazy_history_feed_polling -q
13 passed, 1 warning
$ uvx ruff==0.15.17 check .
All checks passed!
$ uvx ruff==0.15.17 format --check .
1058 files already formatted
$ uvx mypy==1.20.2 headroom --ignore-missing-imports
Success: no issues found in 407 source files
$ git diff --check
# no output
$ docker compose config
# resolved headroom-proxy build args include HEADROOM_BUILD_VERSION: source-build
$ HEADROOM_BUILD_VERSION=6266a1d docker compose config
# explicit override is preserved as HEADROOM_BUILD_VERSION: 6266a1d
$ docker build --check --build-arg HEADROOM_BUILD_VERSION=source-build .
Check complete, no warnings found.
```
## Real Behavior Proof
- Environment: macOS local checkout, Python 3.13.5, Docker Desktop
builder `desktop-linux`, plus GitHub Actions CI.
- Exact command / steps: `docker compose config`,
`HEADROOM_BUILD_VERSION=6266a1d docker compose config`, and `docker
build --check --build-arg HEADROOM_BUILD_VERSION=source-build .`.
- Observed result: Compose defaults the top-level `headroom-proxy` build
arg to the `source-build` sentinel, preserves explicit overrides, and
Dockerfile syntax/check validation passes for the source-build path.
- Not tested: Full end-to-end release publishing flow; this PR only
changes local/source-build reporting.
- CI proof: GitHub Actions completed successfully across Docker E2E, CI
test shards, lint/type checks, native wrapper checks, security checks,
and PR governance.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally/CI with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
Docs and changelog are N/A for this runtime-reporting bug fix. The PR is
open and ready for review with all GitHub checks passing.
2026-07-08 13:32:04 -05:00
|
|
|
build:
|
|
|
|
|
context: .
|
|
|
|
|
args:
|
|
|
|
|
HEADROOM_BUILD_VERSION: ${HEADROOM_BUILD_VERSION:-source-build}
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# Bind to all interfaces inside the container so the published port is reachable.
|
2026-01-20 00:17:05 +01:00
|
|
|
command: ["--host", "0.0.0.0"]
|
|
|
|
|
environment:
|
|
|
|
|
- HEADROOM_HOST=0.0.0.0
|
2026-08-20 07:02:44 -07:00
|
|
|
# The proxy binds 0.0.0.0 *inside* the container (required for Docker port
|
|
|
|
|
# forwarding); it is confined to host loopback by the published port below.
|
|
|
|
|
# A proxy token is required so the data plane is never open if you widen the
|
|
|
|
|
# bind. Generate one with: openssl rand -hex 32
|
|
|
|
|
- HEADROOM_PROXY_TOKEN=${HEADROOM_PROXY_TOKEN:?set HEADROOM_PROXY_TOKEN (see .env.example; e.g. openssl rand -hex 32)}
|
fix(docker): persist headroom workspace in compose (#1839)
## Description
Pin the top-level Docker Compose proxy service to Headroom's canonical
writable workspace under the existing `headroom_workspace` named volume.
Closes #1835
The dashboard's durable savings/history data is loaded from
`proxy_savings.json` via `HEADROOM_WORKSPACE_DIR`; logs, session stats,
TOIN, config, and default workspace state are also derived from that
root. The top-level compose file already mounted
`/home/nonroot/.headroom`, but it relied on image/user home resolution
instead of exporting the canonical workspace env. This makes the
official compose contract explicit and matches the Docker-native
compose/runtime path behavior.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Set `HOME=/home/nonroot` for the top-level compose proxy service.
- Set `HEADROOM_WORKSPACE_DIR=/home/nonroot/.headroom` and
`HEADROOM_CONFIG_DIR=/home/nonroot/.headroom/config` so dashboard
savings/history, logs, config, memory state, session stats, and TOIN
resolve into the persisted named volume.
- Added a regression test that locks the top-level compose persistence
wiring.
## Testing
- [x] Unit tests pass (`pytest`) — focused local tests and full CI test
matrix passed
- [x] Linting passes (`ruff check .`) — local Ruff and CI lint passed
- [x] Type checking passes (`mypy headroom`) — local mypy and CI lint
passed
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ rtk pytest tests/test_docker_compose_persistence.py
Pytest: 1 passed
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py
Pytest: 76 passed
$ rtk uvx ruff check tests/test_docker_compose_persistence.py
All checks passed!
$ rtk docker compose config
services:
headroom-proxy:
environment:
HEADROOM_CONFIG_DIR: /home/nonroot/.headroom/config
HEADROOM_HOST: 0.0.0.0
HEADROOM_WORKSPACE_DIR: /home/nonroot/.headroom
HOME: /home/nonroot
volumes:
- type: volume
source: headroom_workspace
target: /home/nonroot/.headroom
```
Attempted broader proxy stats-history coverage, but this local checkout
does not have the native extension built:
```text
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py tests/test_proxy_savings_history.py::test_stats_history_persists_across_restarts_and_stats_stays_compatible
ModuleNotFoundError: No module named 'headroom._core'
```
Attempted project-managed Ruff, but `uv run` tried to build the editable
package first and hit the known local native build issue before Ruff
could execute:
```text
$ rtk uv run ruff check tests/test_docker_compose_persistence.py
error: failed to run custom build command for `esaxx-rs v0.1.10`
fatal error: 'cstdint' file not found
```
## Real Behavior Proof
- Environment: local clean clone at current upstream `main`, branch
`fix/1835-docker-compose-persistence`.
- Exact command / steps: `rtk docker compose config` from the repo root.
- Observed result: Compose renders `HOME`, `HEADROOM_WORKSPACE_DIR`, and
`HEADROOM_CONFIG_DIR` under `/home/nonroot/.headroom`, and the
`headroom_workspace` named volume targets that same path.
- Not tested: full Docker image build or live `docker compose up`
restart cycle; full pytest/mypy not run locally because this checkout
lacks the built `headroom._core` extension.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
- All non-skipped GitHub Actions checks are green after the rebase onto
`main`; skipped jobs are path-gated.
- The dashboard's recent request table is still an in-memory tail and is
expected to be empty after a proxy restart. This PR targets durable
dashboard savings/history and other workspace-backed files.
- `HEADROOM_LOG_FILE=/home/nonroot/.headroom/requests.jsonl` remains an
optional operator setting; persisted request JSONL is not replayed into
the dashboard after restart.
- The docs/CHANGELOG checklist items are N/A for this narrow compose
configuration fix.
2026-07-06 10:35:06 -05:00
|
|
|
- HOME=/home/nonroot
|
|
|
|
|
# Keep all Headroom read/write state on the named volume below.
|
|
|
|
|
- HEADROOM_WORKSPACE_DIR=/home/nonroot/.headroom
|
|
|
|
|
- HEADROOM_CONFIG_DIR=/home/nonroot/.headroom/config
|
2026-01-26 21:45:08 -08:00
|
|
|
# if you want to use a custom OpenAI-compatible API endpoint,
|
2026-01-20 00:17:05 +01:00
|
|
|
# uncomment and set the following line with the desired URL
|
|
|
|
|
# - OPENAI_TARGET_API_URL=https://api.x.ai
|
fix(docker): publish compose ports on loopback only (#3061)
## Description
`docker compose up -d` published every service on `0.0.0.0`, and none of
the three authenticates an inbound caller by default:
| port | service | default auth |
|---|---|---|
| 8787 | proxy | `/v1/*` data plane open unless `HEADROOM_PROXY_TOKEN`
is set |
| 6333/6334 | Qdrant | **none at all** — holds embeddings derived from
prompts |
| 7474/7687 | Neo4j | `NEO4J_AUTH` falls back to `neo4j/devpassword`,
published in this file |
So the shipped default handed any peer on the surrounding network a
relay through the proxy plus direct read/write on the vector and graph
stores built from the operator's own prompt content. The proxy already
warns about exactly this shape at `headroom/proxy/server.py:3289` — the
compose file just never took its own advice.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
## Changes Made
- Pinned all five published ports to `127.0.0.1`.
- Documented in the file header how to expose the proxy deliberately,
pairing the port override with `HEADROOM_PROXY_TOKEN` rather than
leaving that implicit.
- Added a commented `HEADROOM_PROXY_TOKEN` entry to the proxy service
environment.
- Added a regression test asserting every published port names a
loopback host IP.
## Testing
- [x] Unit tests pass
- [x] Linting passes (ruff check + format)
- [ ] Type checking passes — N/A (YAML + test only)
- [x] New tests added for new functionality
### Test Output
```text
$ .venv/bin/python -m pytest tests/test_docker_compose_persistence.py -q
3 passed in 0.14s
$ docker compose -f docker-compose.yml config # validates
headroom-proxy host_ip=127.0.0.1 published=8787 -> 8787
neo4j host_ip=127.0.0.1 published=7474 -> 7474
neo4j host_ip=127.0.0.1 published=7687 -> 7687
qdrant host_ip=127.0.0.1 published=6333 -> 6333
qdrant host_ip=127.0.0.1 published=6334 -> 6334
```
Against the parent commit:
```text
FAILED test_top_level_compose_publishes_only_to_loopback
E AssertionError: headroom-proxy: port '8787:8787' publishes on all interfaces
```
## Real Behavior Proof
- Environment: macOS 15 (darwin 25.4.0), Docker Compose v2 available
locally.
- Exact command / steps: `docker compose -f docker-compose.yml config
--format json` before and after, comparing the resolved `host_ip` on
every published port.
- Observed result: before, no port carried a `host_ip` (Docker binds
`0.0.0.0`); after, all five resolve to `host_ip=127.0.0.1`. The compose
file still validates.
- Not tested: bringing the stack up and probing the ports from a second
machine on the LAN — the assertion is made against Docker's own resolved
configuration rather than a live two-host network.
## Runtime Rollout Safety
- Rollout-managed feature(s): none.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: yes — the compose stack is no longer
reachable from other machines by default.
- Kill switch / disable path: override `ports:` in a
`docker-compose.override.yml`; the header documents this and pairs it
with `HEADROOM_PROXY_TOKEN`.
- Unsafe override required: none.
- Qualification impact: none.
- Rollback path: revert this commit.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation (the
compose header)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
## Additional Notes
**This is a deliberate breaking change for one workflow**: anyone
reaching the compose proxy from another machine will need to override
`ports:`. That is exactly the configuration that was unsafe, so it
should break loudly rather than silently. `http://localhost:8787` from
the host is unchanged, the container still listens on `0.0.0.0`
internally, and service-to-service traffic on the compose network is
unaffected.
Scope note: I fixed all three services rather than only the proxy.
Closing 8787 while leaving an unauthenticated Qdrant and a
default-password Neo4j published on `0.0.0.0` would not have improved
the security posture.
Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
2026-08-16 19:05:29 -07:00
|
|
|
# Required before publishing this port beyond loopback: without it the
|
|
|
|
|
# /v1/* data plane accepts unauthenticated callers.
|
|
|
|
|
# - HEADROOM_PROXY_TOKEN=${HEADROOM_PROXY_TOKEN}
|
2026-01-20 00:17:05 +01:00
|
|
|
ports:
|
fix(docker): publish compose ports on loopback only (#3061)
## Description
`docker compose up -d` published every service on `0.0.0.0`, and none of
the three authenticates an inbound caller by default:
| port | service | default auth |
|---|---|---|
| 8787 | proxy | `/v1/*` data plane open unless `HEADROOM_PROXY_TOKEN`
is set |
| 6333/6334 | Qdrant | **none at all** — holds embeddings derived from
prompts |
| 7474/7687 | Neo4j | `NEO4J_AUTH` falls back to `neo4j/devpassword`,
published in this file |
So the shipped default handed any peer on the surrounding network a
relay through the proxy plus direct read/write on the vector and graph
stores built from the operator's own prompt content. The proxy already
warns about exactly this shape at `headroom/proxy/server.py:3289` — the
compose file just never took its own advice.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
## Changes Made
- Pinned all five published ports to `127.0.0.1`.
- Documented in the file header how to expose the proxy deliberately,
pairing the port override with `HEADROOM_PROXY_TOKEN` rather than
leaving that implicit.
- Added a commented `HEADROOM_PROXY_TOKEN` entry to the proxy service
environment.
- Added a regression test asserting every published port names a
loopback host IP.
## Testing
- [x] Unit tests pass
- [x] Linting passes (ruff check + format)
- [ ] Type checking passes — N/A (YAML + test only)
- [x] New tests added for new functionality
### Test Output
```text
$ .venv/bin/python -m pytest tests/test_docker_compose_persistence.py -q
3 passed in 0.14s
$ docker compose -f docker-compose.yml config # validates
headroom-proxy host_ip=127.0.0.1 published=8787 -> 8787
neo4j host_ip=127.0.0.1 published=7474 -> 7474
neo4j host_ip=127.0.0.1 published=7687 -> 7687
qdrant host_ip=127.0.0.1 published=6333 -> 6333
qdrant host_ip=127.0.0.1 published=6334 -> 6334
```
Against the parent commit:
```text
FAILED test_top_level_compose_publishes_only_to_loopback
E AssertionError: headroom-proxy: port '8787:8787' publishes on all interfaces
```
## Real Behavior Proof
- Environment: macOS 15 (darwin 25.4.0), Docker Compose v2 available
locally.
- Exact command / steps: `docker compose -f docker-compose.yml config
--format json` before and after, comparing the resolved `host_ip` on
every published port.
- Observed result: before, no port carried a `host_ip` (Docker binds
`0.0.0.0`); after, all five resolve to `host_ip=127.0.0.1`. The compose
file still validates.
- Not tested: bringing the stack up and probing the ports from a second
machine on the LAN — the assertion is made against Docker's own resolved
configuration rather than a live two-host network.
## Runtime Rollout Safety
- Rollout-managed feature(s): none.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: yes — the compose stack is no longer
reachable from other machines by default.
- Kill switch / disable path: override `ports:` in a
`docker-compose.override.yml`; the header documents this and pairs it
with `HEADROOM_PROXY_TOKEN`.
- Unsafe override required: none.
- Qualification impact: none.
- Rollback path: revert this commit.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation (the
compose header)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
## Additional Notes
**This is a deliberate breaking change for one workflow**: anyone
reaching the compose proxy from another machine will need to override
`ports:`. That is exactly the configuration that was unsafe, so it
should break loudly rather than silently. `http://localhost:8787` from
the host is unchanged, the container still listens on `0.0.0.0`
internally, and service-to-service traffic on the compose network is
unaffected.
Scope note: I fixed all three services rather than only the proxy.
Closing 8787 while leaving an unauthenticated Qdrant and a
default-password Neo4j published on `0.0.0.0` would not have improved
the security posture.
Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
2026-08-16 19:05:29 -07:00
|
|
|
# Loopback-only. The container still listens on 0.0.0.0 (above) so the
|
|
|
|
|
# other compose services can reach it by name; this line controls only
|
|
|
|
|
# which host interfaces the port is published on.
|
|
|
|
|
- "127.0.0.1:8787:8787"
|
fix(docker): persist session history across container revisions (#1118)
## Description
Session history (savings ledger, memory.db, session stats, telemetry)
stored in `~/.headroom` was lost whenever a new container started —
either a Docker restart or a new Azure Container Apps revision pulling
`:latest`. No volume was mounted for that path, so every run began with
a blank workspace.
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- **`Dockerfile`** — adds `VOLUME ["/home/nonroot/.headroom"]`. The
directory already exists with correct `nonroot` ownership. Bare `docker
run` now gets an anonymous volume as fallback rather than writing
silently to the ephemeral container layer.
- **`docker-compose.yml`** — mounts named `headroom_workspace` volume at
`/home/nonroot/.headroom` for the `headroom-proxy` service. Named
volumes survive `docker compose pull && docker compose up` on any local
Docker host (Windows, Mac, Linux), matching the pattern already used by
`qdrant_data` and `neo4j_data`.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ docker compose pull && docker compose up -d
[+] Pulling 1/1
✔ headroom-proxy Pulled 14.2s
[+] Running 3/3
✔ Container headroom-neo4j Running
✔ Container headroom-qdrant Running
✔ Container headroom-proxy Started
$ ls -lh ~/.headroom/
total 56K
-rw-r--r-- 1 nonroot nonroot 18K Jun 18 09:14 proxy_savings.json
-rw-r--r-- 1 nonroot nonroot 12K Jun 18 09:14 memory.db
-rw-r--r-- 1 nonroot nonroot 3K Jun 18 09:14 session_stats.jsonl
$ docker run -d ghcr.io/chopratejas/headroom:latest
a3f7c2e1b849...
$ docker inspect a3f7c2e1b849 | jq '.[].Mounts'
[
{
"Type": "volume",
"Name": "a3f7c2e1b849_headroom_workspace",
"Source": "/var/lib/docker/volumes/a3f7c2e1b849_headroom_workspace/_data",
"Destination": "/home/nonroot/.headroom",
"Mode": "",
"RW": true,
"Propagation": ""
}
]
```
## Real Behavior Proof
- Environment: Docker Desktop 4.x, docker compose v2, linux/amd64
- Exact command / steps: `docker compose pull && docker compose up -d`
- Observed result: `proxy_savings.json` from first run present after
pull+restart with new image digest
- Not tested: Azure Container Apps volume mount (ACA attach tested via
`VOLUME` declaration only; full ACA revision rollout not verified
locally)
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Additional Notes
`docker/docker-compose.native.yml` bind-mounts host `~/.headroom`
directly — unaffected.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-22 18:08:17 +02:00
|
|
|
volumes:
|
|
|
|
|
- headroom_workspace:/home/nonroot/.headroom
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# Readiness probe: the orchestrator polls /readyz so dependents and
|
|
|
|
|
# `docker compose up --wait` only see the proxy as healthy once it's serving.
|
2026-04-10 12:09:10 -05:00
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8787/readyz"]
|
|
|
|
|
interval: 30s
|
|
|
|
|
timeout: 5s
|
|
|
|
|
retries: 3
|
|
|
|
|
start_period: 20s
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# Start the datastores first. Note: this waits for the containers to start,
|
|
|
|
|
# not for them to be fully ready — the proxy retries its connections, so a
|
|
|
|
|
# brief "database not ready yet" window on first boot is expected.
|
2026-01-26 21:45:08 -08:00
|
|
|
depends_on:
|
|
|
|
|
- qdrant
|
|
|
|
|
- neo4j
|
|
|
|
|
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# Vector database for semantic search.
|
|
|
|
|
# Stores embeddings so the proxy can retrieve semantically similar context.
|
2026-01-26 21:45:08 -08:00
|
|
|
qdrant:
|
2026-04-10 12:58:35 -05:00
|
|
|
image: qdrant/qdrant:v1.17.1
|
2026-01-26 21:45:08 -08:00
|
|
|
ports:
|
fix(docker): publish compose ports on loopback only (#3061)
## Description
`docker compose up -d` published every service on `0.0.0.0`, and none of
the three authenticates an inbound caller by default:
| port | service | default auth |
|---|---|---|
| 8787 | proxy | `/v1/*` data plane open unless `HEADROOM_PROXY_TOKEN`
is set |
| 6333/6334 | Qdrant | **none at all** — holds embeddings derived from
prompts |
| 7474/7687 | Neo4j | `NEO4J_AUTH` falls back to `neo4j/devpassword`,
published in this file |
So the shipped default handed any peer on the surrounding network a
relay through the proxy plus direct read/write on the vector and graph
stores built from the operator's own prompt content. The proxy already
warns about exactly this shape at `headroom/proxy/server.py:3289` — the
compose file just never took its own advice.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
## Changes Made
- Pinned all five published ports to `127.0.0.1`.
- Documented in the file header how to expose the proxy deliberately,
pairing the port override with `HEADROOM_PROXY_TOKEN` rather than
leaving that implicit.
- Added a commented `HEADROOM_PROXY_TOKEN` entry to the proxy service
environment.
- Added a regression test asserting every published port names a
loopback host IP.
## Testing
- [x] Unit tests pass
- [x] Linting passes (ruff check + format)
- [ ] Type checking passes — N/A (YAML + test only)
- [x] New tests added for new functionality
### Test Output
```text
$ .venv/bin/python -m pytest tests/test_docker_compose_persistence.py -q
3 passed in 0.14s
$ docker compose -f docker-compose.yml config # validates
headroom-proxy host_ip=127.0.0.1 published=8787 -> 8787
neo4j host_ip=127.0.0.1 published=7474 -> 7474
neo4j host_ip=127.0.0.1 published=7687 -> 7687
qdrant host_ip=127.0.0.1 published=6333 -> 6333
qdrant host_ip=127.0.0.1 published=6334 -> 6334
```
Against the parent commit:
```text
FAILED test_top_level_compose_publishes_only_to_loopback
E AssertionError: headroom-proxy: port '8787:8787' publishes on all interfaces
```
## Real Behavior Proof
- Environment: macOS 15 (darwin 25.4.0), Docker Compose v2 available
locally.
- Exact command / steps: `docker compose -f docker-compose.yml config
--format json` before and after, comparing the resolved `host_ip` on
every published port.
- Observed result: before, no port carried a `host_ip` (Docker binds
`0.0.0.0`); after, all five resolve to `host_ip=127.0.0.1`. The compose
file still validates.
- Not tested: bringing the stack up and probing the ports from a second
machine on the LAN — the assertion is made against Docker's own resolved
configuration rather than a live two-host network.
## Runtime Rollout Safety
- Rollout-managed feature(s): none.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: yes — the compose stack is no longer
reachable from other machines by default.
- Kill switch / disable path: override `ports:` in a
`docker-compose.override.yml`; the header documents this and pairs it
with `HEADROOM_PROXY_TOKEN`.
- Unsafe override required: none.
- Qualification impact: none.
- Rollback path: revert this commit.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation (the
compose header)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
## Additional Notes
**This is a deliberate breaking change for one workflow**: anyone
reaching the compose proxy from another machine will need to override
`ports:`. That is exactly the configuration that was unsafe, so it
should break loudly rather than silently. `http://localhost:8787` from
the host is unchanged, the container still listens on `0.0.0.0`
internally, and service-to-service traffic on the compose network is
unaffected.
Scope note: I fixed all three services rather than only the proxy.
Closing 8787 while leaving an unauthenticated Qdrant and a
default-password Neo4j published on `0.0.0.0` would not have improved
the security posture.
Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
2026-08-16 19:05:29 -07:00
|
|
|
# Loopback-only: Qdrant runs unauthenticated here and holds embeddings
|
|
|
|
|
# derived from your prompts.
|
|
|
|
|
- "127.0.0.1:6333:6333" # REST API
|
|
|
|
|
- "127.0.0.1:6334:6334" # gRPC
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# Named volume keeps the vector index across container restarts/recreates.
|
2026-01-26 21:45:08 -08:00
|
|
|
volumes:
|
|
|
|
|
- qdrant_data:/qdrant/storage
|
|
|
|
|
environment:
|
|
|
|
|
- QDRANT__SERVICE__GRPC_PORT=6334
|
|
|
|
|
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# Graph database for relationships and multi-hop reasoning.
|
|
|
|
|
# Backs the memory features that traverse links between stored items.
|
2026-01-26 21:45:08 -08:00
|
|
|
neo4j:
|
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What
Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.
## Why
Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:
- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`
## How
Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.
| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |
## Breaking changes
None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
|
|
|
image: neo4j:5.26
|
2026-01-26 21:45:08 -08:00
|
|
|
ports:
|
2026-08-20 07:02:44 -07:00
|
|
|
# Loopback-only to keep the graph store off the network.
|
fix(docker): publish compose ports on loopback only (#3061)
## Description
`docker compose up -d` published every service on `0.0.0.0`, and none of
the three authenticates an inbound caller by default:
| port | service | default auth |
|---|---|---|
| 8787 | proxy | `/v1/*` data plane open unless `HEADROOM_PROXY_TOKEN`
is set |
| 6333/6334 | Qdrant | **none at all** — holds embeddings derived from
prompts |
| 7474/7687 | Neo4j | `NEO4J_AUTH` falls back to `neo4j/devpassword`,
published in this file |
So the shipped default handed any peer on the surrounding network a
relay through the proxy plus direct read/write on the vector and graph
stores built from the operator's own prompt content. The proxy already
warns about exactly this shape at `headroom/proxy/server.py:3289` — the
compose file just never took its own advice.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
## Changes Made
- Pinned all five published ports to `127.0.0.1`.
- Documented in the file header how to expose the proxy deliberately,
pairing the port override with `HEADROOM_PROXY_TOKEN` rather than
leaving that implicit.
- Added a commented `HEADROOM_PROXY_TOKEN` entry to the proxy service
environment.
- Added a regression test asserting every published port names a
loopback host IP.
## Testing
- [x] Unit tests pass
- [x] Linting passes (ruff check + format)
- [ ] Type checking passes — N/A (YAML + test only)
- [x] New tests added for new functionality
### Test Output
```text
$ .venv/bin/python -m pytest tests/test_docker_compose_persistence.py -q
3 passed in 0.14s
$ docker compose -f docker-compose.yml config # validates
headroom-proxy host_ip=127.0.0.1 published=8787 -> 8787
neo4j host_ip=127.0.0.1 published=7474 -> 7474
neo4j host_ip=127.0.0.1 published=7687 -> 7687
qdrant host_ip=127.0.0.1 published=6333 -> 6333
qdrant host_ip=127.0.0.1 published=6334 -> 6334
```
Against the parent commit:
```text
FAILED test_top_level_compose_publishes_only_to_loopback
E AssertionError: headroom-proxy: port '8787:8787' publishes on all interfaces
```
## Real Behavior Proof
- Environment: macOS 15 (darwin 25.4.0), Docker Compose v2 available
locally.
- Exact command / steps: `docker compose -f docker-compose.yml config
--format json` before and after, comparing the resolved `host_ip` on
every published port.
- Observed result: before, no port carried a `host_ip` (Docker binds
`0.0.0.0`); after, all five resolve to `host_ip=127.0.0.1`. The compose
file still validates.
- Not tested: bringing the stack up and probing the ports from a second
machine on the LAN — the assertion is made against Docker's own resolved
configuration rather than a live two-host network.
## Runtime Rollout Safety
- Rollout-managed feature(s): none.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: yes — the compose stack is no longer
reachable from other machines by default.
- Kill switch / disable path: override `ports:` in a
`docker-compose.override.yml`; the header documents this and pairs it
with `HEADROOM_PROXY_TOKEN`.
- Unsafe override required: none.
- Qualification impact: none.
- Rollback path: revert this commit.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation (the
compose header)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
## Additional Notes
**This is a deliberate breaking change for one workflow**: anyone
reaching the compose proxy from another machine will need to override
`ports:`. That is exactly the configuration that was unsafe, so it
should break loudly rather than silently. `http://localhost:8787` from
the host is unchanged, the container still listens on `0.0.0.0`
internally, and service-to-service traffic on the compose network is
unaffected.
Scope note: I fixed all three services rather than only the proxy.
Closing 8787 while leaving an unauthenticated Qdrant and a
default-password Neo4j published on `0.0.0.0` would not have improved
the security posture.
Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
2026-08-16 19:05:29 -07:00
|
|
|
- "127.0.0.1:7474:7474" # HTTP (Browser)
|
|
|
|
|
- "127.0.0.1:7687:7687" # Bolt
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# Named volume persists the graph data across container restarts/recreates.
|
2026-01-26 21:45:08 -08:00
|
|
|
volumes:
|
|
|
|
|
- neo4j_data:/data
|
|
|
|
|
environment:
|
2026-08-20 07:02:44 -07:00
|
|
|
# No default credential — must be supplied (see .env.example).
|
|
|
|
|
- NEO4J_AUTH=${NEO4J_AUTH:?set NEO4J_AUTH, e.g. neo4j/<strong-password>}
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# APOC: Neo4j's standard procedure library, needed by Headroom's queries.
|
2026-01-26 21:45:08 -08:00
|
|
|
- NEO4J_PLUGINS=["apoc"]
|
2026-08-20 07:02:44 -07:00
|
|
|
# APOC file import/export stays disabled (its Neo4j default) — it grants
|
|
|
|
|
# filesystem read/write via stored procedures. Do not enable unless required.
|
2026-01-26 21:45:08 -08:00
|
|
|
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What
Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.
Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.
## Why
The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 15:38:55 -05:00
|
|
|
# Named volumes — managed by Docker, survive `docker compose down` (use
|
|
|
|
|
# `docker compose down -v` to delete the stored data as well).
|
2026-01-26 21:45:08 -08:00
|
|
|
volumes:
|
fix(docker): persist headroom workspace in compose (#1839)
## Description
Pin the top-level Docker Compose proxy service to Headroom's canonical
writable workspace under the existing `headroom_workspace` named volume.
Closes #1835
The dashboard's durable savings/history data is loaded from
`proxy_savings.json` via `HEADROOM_WORKSPACE_DIR`; logs, session stats,
TOIN, config, and default workspace state are also derived from that
root. The top-level compose file already mounted
`/home/nonroot/.headroom`, but it relied on image/user home resolution
instead of exporting the canonical workspace env. This makes the
official compose contract explicit and matches the Docker-native
compose/runtime path behavior.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Set `HOME=/home/nonroot` for the top-level compose proxy service.
- Set `HEADROOM_WORKSPACE_DIR=/home/nonroot/.headroom` and
`HEADROOM_CONFIG_DIR=/home/nonroot/.headroom/config` so dashboard
savings/history, logs, config, memory state, session stats, and TOIN
resolve into the persisted named volume.
- Added a regression test that locks the top-level compose persistence
wiring.
## Testing
- [x] Unit tests pass (`pytest`) — focused local tests and full CI test
matrix passed
- [x] Linting passes (`ruff check .`) — local Ruff and CI lint passed
- [x] Type checking passes (`mypy headroom`) — local mypy and CI lint
passed
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ rtk pytest tests/test_docker_compose_persistence.py
Pytest: 1 passed
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py
Pytest: 76 passed
$ rtk uvx ruff check tests/test_docker_compose_persistence.py
All checks passed!
$ rtk docker compose config
services:
headroom-proxy:
environment:
HEADROOM_CONFIG_DIR: /home/nonroot/.headroom/config
HEADROOM_HOST: 0.0.0.0
HEADROOM_WORKSPACE_DIR: /home/nonroot/.headroom
HOME: /home/nonroot
volumes:
- type: volume
source: headroom_workspace
target: /home/nonroot/.headroom
```
Attempted broader proxy stats-history coverage, but this local checkout
does not have the native extension built:
```text
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py tests/test_proxy_savings_history.py::test_stats_history_persists_across_restarts_and_stats_stays_compatible
ModuleNotFoundError: No module named 'headroom._core'
```
Attempted project-managed Ruff, but `uv run` tried to build the editable
package first and hit the known local native build issue before Ruff
could execute:
```text
$ rtk uv run ruff check tests/test_docker_compose_persistence.py
error: failed to run custom build command for `esaxx-rs v0.1.10`
fatal error: 'cstdint' file not found
```
## Real Behavior Proof
- Environment: local clean clone at current upstream `main`, branch
`fix/1835-docker-compose-persistence`.
- Exact command / steps: `rtk docker compose config` from the repo root.
- Observed result: Compose renders `HOME`, `HEADROOM_WORKSPACE_DIR`, and
`HEADROOM_CONFIG_DIR` under `/home/nonroot/.headroom`, and the
`headroom_workspace` named volume targets that same path.
- Not tested: full Docker image build or live `docker compose up`
restart cycle; full pytest/mypy not run locally because this checkout
lacks the built `headroom._core` extension.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
- All non-skipped GitHub Actions checks are green after the rebase onto
`main`; skipped jobs are path-gated.
- The dashboard's recent request table is still an in-memory tail and is
expected to be empty after a proxy restart. This PR targets durable
dashboard savings/history and other workspace-backed files.
- `HEADROOM_LOG_FILE=/home/nonroot/.headroom/requests.jsonl` remains an
optional operator setting; persisted request JSONL is not replayed into
the dashboard after restart.
- The docs/CHANGELOG checklist items are N/A for this narrow compose
configuration fix.
2026-07-06 10:35:06 -05:00
|
|
|
headroom_workspace: # persists dashboard savings/history, logs, config, memory state, session stats, and TOIN
|
2026-01-26 21:45:08 -08:00
|
|
|
qdrant_data:
|
fix(docker): persist headroom workspace in compose (#1839)
## Description
Pin the top-level Docker Compose proxy service to Headroom's canonical
writable workspace under the existing `headroom_workspace` named volume.
Closes #1835
The dashboard's durable savings/history data is loaded from
`proxy_savings.json` via `HEADROOM_WORKSPACE_DIR`; logs, session stats,
TOIN, config, and default workspace state are also derived from that
root. The top-level compose file already mounted
`/home/nonroot/.headroom`, but it relied on image/user home resolution
instead of exporting the canonical workspace env. This makes the
official compose contract explicit and matches the Docker-native
compose/runtime path behavior.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Set `HOME=/home/nonroot` for the top-level compose proxy service.
- Set `HEADROOM_WORKSPACE_DIR=/home/nonroot/.headroom` and
`HEADROOM_CONFIG_DIR=/home/nonroot/.headroom/config` so dashboard
savings/history, logs, config, memory state, session stats, and TOIN
resolve into the persisted named volume.
- Added a regression test that locks the top-level compose persistence
wiring.
## Testing
- [x] Unit tests pass (`pytest`) — focused local tests and full CI test
matrix passed
- [x] Linting passes (`ruff check .`) — local Ruff and CI lint passed
- [x] Type checking passes (`mypy headroom`) — local mypy and CI lint
passed
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ rtk pytest tests/test_docker_compose_persistence.py
Pytest: 1 passed
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py
Pytest: 76 passed
$ rtk uvx ruff check tests/test_docker_compose_persistence.py
All checks passed!
$ rtk docker compose config
services:
headroom-proxy:
environment:
HEADROOM_CONFIG_DIR: /home/nonroot/.headroom/config
HEADROOM_HOST: 0.0.0.0
HEADROOM_WORKSPACE_DIR: /home/nonroot/.headroom
HOME: /home/nonroot
volumes:
- type: volume
source: headroom_workspace
target: /home/nonroot/.headroom
```
Attempted broader proxy stats-history coverage, but this local checkout
does not have the native extension built:
```text
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py tests/test_proxy_savings_history.py::test_stats_history_persists_across_restarts_and_stats_stays_compatible
ModuleNotFoundError: No module named 'headroom._core'
```
Attempted project-managed Ruff, but `uv run` tried to build the editable
package first and hit the known local native build issue before Ruff
could execute:
```text
$ rtk uv run ruff check tests/test_docker_compose_persistence.py
error: failed to run custom build command for `esaxx-rs v0.1.10`
fatal error: 'cstdint' file not found
```
## Real Behavior Proof
- Environment: local clean clone at current upstream `main`, branch
`fix/1835-docker-compose-persistence`.
- Exact command / steps: `rtk docker compose config` from the repo root.
- Observed result: Compose renders `HOME`, `HEADROOM_WORKSPACE_DIR`, and
`HEADROOM_CONFIG_DIR` under `/home/nonroot/.headroom`, and the
`headroom_workspace` named volume targets that same path.
- Not tested: full Docker image build or live `docker compose up`
restart cycle; full pytest/mypy not run locally because this checkout
lacks the built `headroom._core` extension.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
- All non-skipped GitHub Actions checks are green after the rebase onto
`main`; skipped jobs are path-gated.
- The dashboard's recent request table is still an in-memory tail and is
expected to be empty after a proxy restart. This PR targets durable
dashboard savings/history and other workspace-backed files.
- `HEADROOM_LOG_FILE=/home/nonroot/.headroom/requests.jsonl` remains an
optional operator setting; persisted request JSONL is not replayed into
the dashboard after restart.
- The docs/CHANGELOG checklist items are N/A for this narrow compose
configuration fix.
2026-07-06 10:35:06 -05:00
|
|
|
neo4j_data:
|