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.
This commit is contained in:
Vinay Gupta 2026-07-06 10:35:06 -05:00 committed by GitHub
parent e22d7453d4
commit 5e29c06aaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 22 additions and 2 deletions

View file

@ -4,6 +4,10 @@ services:
command: ["--host", "0.0.0.0"] command: ["--host", "0.0.0.0"]
environment: environment:
- HEADROOM_HOST=0.0.0.0 - HEADROOM_HOST=0.0.0.0
- 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
# if you want to use a custom OpenAI-compatible API endpoint, # if you want to use a custom OpenAI-compatible API endpoint,
# uncomment and set the following line with the desired URL # uncomment and set the following line with the desired URL
# - OPENAI_TARGET_API_URL=https://api.x.ai # - OPENAI_TARGET_API_URL=https://api.x.ai
@ -48,6 +52,6 @@ services:
- NEO4J_apoc_import_file_use__neo4j__config=true - NEO4J_apoc_import_file_use__neo4j__config=true
volumes: volumes:
headroom_workspace: # persists proxy_savings, memory.db, session_stats, toin across image updates headroom_workspace: # persists dashboard savings/history, logs, config, memory state, session stats, and TOIN
qdrant_data: qdrant_data:
neo4j_data: neo4j_data:

View file

@ -0,0 +1,16 @@
"""Regression checks for Docker Compose persistence wiring."""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_top_level_compose_pins_headroom_state_to_named_volume() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
assert "- headroom_workspace:/home/nonroot/.headroom" in compose
assert "- HOME=/home/nonroot" in compose
assert "- HEADROOM_WORKSPACE_DIR=/home/nonroot/.headroom" in compose
assert "- HEADROOM_CONFIG_DIR=/home/nonroot/.headroom/config" in compose