Skip to content

Add isolated Home Assistant development environment with Docker Compose - #183

Merged
lollox80 merged 5 commits into
ha-vimar:mainfrom
andreacasarin:feat/docker-development
Oct 20, 2026
Merged

lollox80 merged 5 commits into
ha-vimar:mainfrom
andreacasarin:feat/docker-development

Conversation

@andreacasarin

@andreacasarin andreacasarin commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What and why

Contributors currently have to install the custom integration and manually onboard a Home Assistant instance before trying setup forms or frontend changes. Add docker/docker-compose.yaml and its supporting files under docker/ so ./docker/dev.sh starts a separate development instance with automatic onboarding and the developer / developer login.

The current checkout is mounted read-only. Running the same command after editing code or switching branches recreates the containers while retaining development configuration and the owner account. HA_DEV_PORT supports an alternative localhost port.

Only a localhost web proxy is published; HA runs on an internal Docker network. No production configuration, QR data or intercom entry is seeded. Bootstrap verifies the integration's initial form without submitting SIP credentials and cancels that flow. Existing entries are retained on subsequent starts.

This contribution includes no SIP transport changes, hardware probes, LAN/media forwarding or firmware/capture material.

Tested on

Docker Desktop on macOS, Docker Compose v5.5.1, pinned Home Assistant 2026.10.1 image. Fresh onboarding and a second start both passed at an alternative localhost port, including owner login, setup-form cancellation and dashboard WebSocket authentication. Verified persistent login/configuration, read-only checkout mount, absence of a configured intercom and absence of an IPv4 default route in HA. A port conflict failed without stopping the existing stack.

Synthetic seed regression tests cover first startup, preservation of credentials/HA storage/user configuration, and restoring a missing template. No live hardware tests, calls or actuator operations were performed. Initial macOS run before the Linux review follow-up (excluding live, media and browser tests): 2313 passed, 1 skipped, 97.11% line/branch coverage. ruff check ., shell syntax and Compose configuration validation passed.

Linux ownership review follow-up

Address the Linux root-owned-state feedback with the accepted cleanup-container approach, retaining the Home Assistant image's normal user/init behavior:

./docker/dev.sh reset

This stops/removes only the selected development Compose project, clears its .dev-ha contents through a one-shot seed container with networking disabled, and removes the empty directory as the host user. No host sudo is needed. Both language guides explain that reset deletes development credentials/configuration/history and that Linux files may remain root-owned until reset. Symlinked state roots are refused; reset does not follow child symlinks outside its mount.

Added reset safety tests and a disposable-checkout Linux Docker Engine lifecycle test in the existing GitHub Actions pytest jobs. The lifecycle test ran and passed, rather than being skipped, on both Python versions: fresh onboarding, restart persistence, confirmed root-owned state, reset, fresh onboarding after reset and final cleanup. Local unit-test runs skip this Docker lifecycle check and do not start containers.

Verified CI run:

  • Python 3.13: 2,316 passed, 13 skipped; 97.15% coverage.
  • Python 3.14: 2,316 passed, 13 skipped; 97.14% coverage.
  • Ruff, HACS and Hassfest passed. Local reset/seed tests: 8 passed, Linux lifecycle check skipped on macOS as intended; shell syntax and diff checks passed.

The owner's retained local development containers stayed stopped and their state was not reset. Production was not accessed or modified.

Docs

Docs: docs/DEVELOPMENT.md, docs/DEVELOPMENT.it.md, both READMEs and CONTRIBUTING.md.

Changelog

Changelog: Other changes - Start an isolated, automatically onboarded Home Assistant development instance from the current checkout with Docker Compose.

@Apeiv

Apeiv commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Read through it: no secrets or plant data, nothing under docker/ reaches the HACS zip, and port 18123 on 127.0.0.1 with its own vimar-intercom-dev project keeps it apart from a real HA.

One question from reading, not running: on Linux the seed and homeassistant containers run as root (no user: in docker-compose.yaml), so .dev-ha/config (mode 700, seed.py:13-16) and .dev-ha/credentials.json (mode 600, seed.py:20-25) end up owned by root. Then git clean -fdx or rm -rf .dev-ha needs sudo, and the host user can't read the credentials. Docker Desktop hides this, which may be why it worked for you. Could you check on a Linux host, or add a sudo rm note to docs/DEVELOPMENT.md?

@lollox80

Copy link
Copy Markdown
Collaborator

Thanks @andreacasarin, this is a welcome addition: a throwaway HA that never touches a real plant is exactly what new contributors were missing. And thanks @Apeiv for the read-through.

+1 on Apeiv's point, and I'd rather fix it than document sudo rm. With umask 077 and mode 700 everywhere, on a Linux host .dev-ha/ and everything Home Assistant writes inside it (.storage, deps, logs) end up owned by root and unreadable, so even ls .dev-ha/config fails, not just the cleanup.

Two ways I'd accept, your choice:

  1. Run as the host user: dev.sh exports DEV_UID=$(id -u) / DEV_GID=$(id -g) and the seed, homeassistant and bootstrap services get user: "${DEV_UID:-1000}:${DEV_GID:-1000}". Please check that the HA image actually starts that way (its s6 init may not like a non-root user); if it does, this is the cleanest.
  2. Clean up from inside: a ./docker/dev.sh reset that runs compose down and then empties .dev-ha from a one-shot container (same image, /state mounted), plus a line in both docs/DEVELOPMENT*.md saying that on Linux this is the way to delete it.

Either way, a run on a Linux host (Docker Engine, not Desktop) in the PR's "Tested on" would close it. The rest looks good from my side: localhost only, internal network, no SIP or plant data, nothing in the HACS zip.

@andreacasarin

andreacasarin commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

That's a good point, thanks @Apeiv!

Addressed the Linux ownership feedback using option 2: ./docker/dev.sh reset @lollox80 .

The command stops/removes the development Compose project, clears .dev-ha through a one-shot container with networking disabled, and removes the empty directory without host sudo. It preserves the HA image’s normal startup behavior. Both language guides document the deletion scope and Linux ownership limitation.

Added safety tests plus a disposable Linux Docker Engine lifecycle test covering fresh onboarding, restart persistence, confirmed root-owned state, reset, and fresh onboarding after reset. That lifecycle test ran and passed in both GitHub Actions Python jobs:

  • Python 3.13: 2,316 passed, 97.15% coverage.
  • Python 3.14: 2,316 passed, 97.14% coverage.

@lollox80 lollox80 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @andreacasarin, the reset approach and the Linux Engine lifecycle run close the ownership point nicely.

One change before merging: test_dev_compose_linux.py runs inside the required pytest (3.13) and pytest (3.14) jobs, so every PR now pulls the HA image from Docker Hub and starts the stack twice. That adds about 2 minutes per job (4 → 6 min, repeated by the merge queue), and a Docker Hub hiccup or rate limit would fail PRs that don't touch docker/.

Could you give it a docker marker excluded by default (like media and browser in addopts) and run it in its own job only when docker/** changes? If you'd rather keep this PR as is, say so and we'll track it in a follow-up issue instead.

Everything else looks good from my side.


Generated by Claude Code

@lollox80 lollox80 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks, this addresses the earlier request well: the docker marker keeps the lifecycle test out of the required pytest jobs (back to ~4 min), and the new workflow is read-only (contents: read, pull_request only, no secrets), path-filtered, time-limited and runs once. Image digests pinned, localhost-only port, internal network, nothing added to the release zip. Looks good.

One thing CI can't show: maintainers also run the default suite on Windows before each PR, and two of the new tests fail there.

  • tests/test_dev_seed.py:45-46 asserts 0o700/0o600, but Windows reports 0o777/0o666. The repo already has an assert_private_mode fixture in tests/conftest.py that skips the check on win32.
  • tests/test_dev_reset.py creates symlinks (:33-35, :53) without a skip, and the launcher test runs bash with a fake docker. Elsewhere we skip when os.symlink raises OSError (see tests/test_ring_log.py).

The simplest fix is a module-level pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="POSIX permissions, symlinks and bash") in both files.

Optional: restrict push in docker-development.yml to branches: [main], so a same-repo branch with an open PR doesn't run the job twice.

With the Windows skips in place this is ready to merge from my side.

@andreacasarin

Copy link
Copy Markdown
Contributor Author

Thanks, agreed—addressed in 407da6c.

The lifecycle test now has a docker marker, excluded by default and explicitly excluded from both required Python jobs. A separate workflow runs it when docker/**, the lifecycle test or its workflow changes. Manual execution is also available.

Validation passed:

  • Dedicated Linux Docker Engine lifecycle: 1 passed.
  • Python 3.13: 2,397 passed, 97.18% coverage.
  • Python 3.14: 2,397 passed, 97.17% coverage.
  • Ruff, HACS and Hassfest: green.

Both development guides and the documented test commands are updated.

@andreacasarin

Copy link
Copy Markdown
Contributor Author

Thanks—addressed in 915770c.
Both development test modules now skip on Windows with the suggested reason. Docker workflow pushes are also restricted to main; path-filtered PR runs and manual execution remain available.
The eight focused tests pass on macOS, and all eight skip with the Windows condition simulated. This was not a native Windows run.

@lollox80
lollox80 enabled auto-merge October 20, 2026 18:14

@lollox80 lollox80 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @andreacasarin, everything addressed: docker marker out of the required jobs, a separate read-only path-filtered workflow, Windows skips. Approving.


Generated by Claude Code

@lollox80
lollox80 added this pull request to the merge queue Oct 20, 2026
Merged via the queue into ha-vimar:main with commit 5749d61 Oct 20, 2026
8 checks passed
@lollox80 lollox80 mentioned this pull request Oct 22, 2026
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.

3 participants