Production deployment via the Ansible/IaC repo; Iris builds and publishes only #104

Open
opened 2026-09-01 15:53:12 +00:00 by claude-bot · 4 comments
Contributor

Goal

Deploy Iris to production the same way the rest of the fleet is deployed: this repo builds and publishes a container image; the Ansible/IaC repo deploys it. The container registry is the boundary between them.

There is currently no production deployment at all — only the dev host (10.1.1.14), which runs :main from docker-compose.registry.yml (#65).

Split of responsibilities

Iris-WLED — builds and publishes, never deploys

Trigger Tags State
merge to main (ci.yml) :main, :sha-<short> working (#64)
v* tag (release.yml) :X.Y.Z, :X.Y, :latest ⚠️ never executed — see #4

Ansible/IaC repo (cloned at /home/ryan/ansible on the control node) — owns everything runtime

  • The role/playbook that pulls a pinned image tag and runs the container
  • .env and secrets: SESSION_SECRET, OIDC_CLIENT_SECRET, OIDC_*, AI_API_KEY, CALENDARIFIC_KEY, LATITUDE / LONGITUDE, WLED_HOST
  • The persistent volume for data/ — SQLite DB and data/backups/, which the 04:00 daily_backup job writes to (#57). This is the one piece whose loss is unrecoverable.
  • Reverse proxy / TLS, and the OIDC_REDIRECT_URI that must match it
  • Host firewall reachability to the WLED controller

Putting the secrets in the IaC repo is a strict improvement on today: on the dev host they are hand-edited, which is how LATITUDE/LONGITUDE/WLED_HOST sat blank until 2026-08-31.

Changes needed in this repo

Small — the build half is nearly done.

  • docs/setup.md §6 reframed: it currently reads as the deployment path. It should be dev-host / self-hoster guidance, with a note that production is deployed from the IaC repo.
  • Same for docker-compose.registry.yml — keep it, label it clearly as the dev/self-host path so nobody assumes it is what prod runs.
  • Confirm whether pulling git.rhoving.com/rbrooks/iris-wled requires authentication. The dev host pulls fine, but it may simply already be logged in — if auth is required, prod needs a read-only pull token, distinct from REGISTRY_TOKEN (which carries write:package).

Recommendations

Pin to an immutable tag — never :latest. :latest moves, so two Ansible runs with no change to the IaC repo can produce different running containers. That defeats the purpose of IaC and makes rollback guesswork. IRIS_TAG already exists as the seam; prod should set :X.Y.Z (or a :sha-<short> / digest), and a deploy becomes "change one variable, re-run the play."

Health check the deploy. The container exposes GET /api/v1/health, already used as a CI gate — the play should wait on it rather than assuming docker compose up -d succeeded.

Back up before upgrading. Iris takes its own pre-bulk-op and daily backups, but an image rollback with a migrated DB is not automatically reversible. The play should snapshot data/ before pulling a new tag.

Blocked on

#4 must actually work first. IaC would consume :X.Y.Z, and only release.yml produces those tags — the workflow that has never run once. Proving the release pipeline is a prerequisite, not a parallel task. #3's latest/version-tag criterion is the same dependency.

Acceptance criteria

  • #4 verified: a tag produces :X.Y.Z in the registry plus a Forgejo release
  • An IaC role deploys Iris from a pinned tag to a prod host, secrets managed there
  • data/ persists across a redeploy, and backups survive
  • The deploy waits on /api/v1/health before reporting success
  • Rolling back is changing one variable and re-running the play
  • docs/setup.md distinguishes the dev/self-host path from production

Related: #3, #4, #10, #65, #7.

## Goal Deploy Iris to production the same way the rest of the fleet is deployed: **this repo builds and publishes a container image; the Ansible/IaC repo deploys it.** The container registry is the boundary between them. There is currently **no production deployment at all** — only the dev host (10.1.1.14), which runs `:main` from `docker-compose.registry.yml` (#65). ## Split of responsibilities **Iris-WLED — builds and publishes, never deploys** | Trigger | Tags | State | |---|---|---| | merge to `main` (`ci.yml`) | `:main`, `:sha-<short>` | ✅ working (#64) | | `v*` tag (`release.yml`) | `:X.Y.Z`, `:X.Y`, `:latest` | ⚠️ **never executed** — see #4 | **Ansible/IaC repo (cloned at `/home/ryan/ansible` on the control node) — owns everything runtime** - The role/playbook that pulls a pinned image tag and runs the container - `.env` and secrets: `SESSION_SECRET`, `OIDC_CLIENT_SECRET`, `OIDC_*`, `AI_API_KEY`, `CALENDARIFIC_KEY`, `LATITUDE` / `LONGITUDE`, `WLED_HOST` - The persistent volume for `data/` — SQLite DB **and** `data/backups/`, which the 04:00 `daily_backup` job writes to (#57). This is the one piece whose loss is unrecoverable. - Reverse proxy / TLS, and the `OIDC_REDIRECT_URI` that must match it - Host firewall reachability to the WLED controller Putting the secrets in the IaC repo is a strict improvement on today: on the dev host they are hand-edited, which is how `LATITUDE`/`LONGITUDE`/`WLED_HOST` sat blank until 2026-08-31. ## Changes needed in *this* repo Small — the build half is nearly done. - [ ] `docs/setup.md` §6 reframed: it currently reads as *the* deployment path. It should be dev-host / self-hoster guidance, with a note that production is deployed from the IaC repo. - [ ] Same for `docker-compose.registry.yml` — keep it, label it clearly as the dev/self-host path so nobody assumes it is what prod runs. - [ ] Confirm whether pulling `git.rhoving.com/rbrooks/iris-wled` requires authentication. The dev host pulls fine, but it may simply already be logged in — if auth is required, prod needs a **read-only** pull token, distinct from `REGISTRY_TOKEN` (which carries `write:package`). ## Recommendations **Pin to an immutable tag — never `:latest`.** `:latest` moves, so two Ansible runs with no change to the IaC repo can produce different running containers. That defeats the purpose of IaC and makes rollback guesswork. `IRIS_TAG` already exists as the seam; prod should set `:X.Y.Z` (or a `:sha-<short>` / digest), and a deploy becomes "change one variable, re-run the play." **Health check the deploy.** The container exposes `GET /api/v1/health`, already used as a CI gate — the play should wait on it rather than assuming `docker compose up -d` succeeded. **Back up before upgrading.** Iris takes its own pre-bulk-op and daily backups, but an image rollback with a migrated DB is not automatically reversible. The play should snapshot `data/` before pulling a new tag. ## Blocked on **#4 must actually work first.** IaC would consume `:X.Y.Z`, and only `release.yml` produces those tags — the workflow that has never run once. Proving the release pipeline is a prerequisite, not a parallel task. #3's `latest`/version-tag criterion is the same dependency. ## Acceptance criteria - [ ] #4 verified: a tag produces `:X.Y.Z` in the registry plus a Forgejo release - [ ] An IaC role deploys Iris from a pinned tag to a prod host, secrets managed there - [ ] `data/` persists across a redeploy, and backups survive - [ ] The deploy waits on `/api/v1/health` before reporting success - [ ] Rolling back is changing one variable and re-running the play - [ ] `docs/setup.md` distinguishes the dev/self-host path from production Related: #3, #4, #10, #65, #7.
Author
Contributor

The IaC repo is Rhoving/iac-repo — note it's under the Rhoving org, not rbrooks. The issue body above refers to it by its control-node path (/home/ryan/ansible) because I couldn't reach the control node from here to read the remote; that path is where it's checked out, and Rhoving/iac-repo is the canonical name.

One consequence worth recording, since it crosses a repo boundary: the deploy role lives in Rhoving/iac-repo, so the acceptance criteria here split across two repos. The Iris-side items (docs, the registry-path labelling, confirming pull auth) can be done here; the role, secrets and volume management land there. If a matching issue is opened on Rhoving/iac-repo, cross-linking it makes the trail readable from both ends — this issue is the Iris half.

**The IaC repo is [`Rhoving/iac-repo`](https://git.rhoving.com/Rhoving/iac-repo)** — note it's under the `Rhoving` org, not `rbrooks`. The issue body above refers to it by its control-node path (`/home/ryan/ansible`) because I couldn't reach the control node from here to read the remote; that path is where it's checked out, and `Rhoving/iac-repo` is the canonical name. One consequence worth recording, since it crosses a repo boundary: **the deploy role lives in `Rhoving/iac-repo`, so the acceptance criteria here split across two repos.** The Iris-side items (docs, the registry-path labelling, confirming pull auth) can be done here; the role, secrets and volume management land there. If a matching issue is opened on `Rhoving/iac-repo`, cross-linking it makes the trail readable from both ends — this issue is the Iris half.
claude-bot added this to the v1.1.0 milestone 2026-09-01 21:54:12 +00:00
Author
Contributor

The three in-repo items are done in #139. Leaving this open — the substance is elsewhere and blocked.

Done

  • docs/setup.md §7 and docker-compose.registry.yml now say plainly that they are the dev-host and self-hoster path, not the production runbook, with a pointer that production is deployed from the IaC repo and the registry is the boundary.

  • The registry question is answered by asking it, not assuming:

    GET /v2/                                                    -> 401
         Www-Authenticate: Bearer realm=".../v2/token", service="container_registry"
    GET /v2/token?...&scope=repository:rbrooks/iris-wled:pull   -> 200, token issued anonymously
    GET /v2/rbrooks/iris-wled/manifests/main  (with that token) -> 200
    

    No authentication is required. The 401 is the standard registry challenge, not a refusal — an anonymous client gets a token that grants pull, because the repo is public. So prod needs no pull secret and no read-only token, which removes one secret from the IaC side entirely. docker login is optional rather than the required step §7 presented it as.

    That holds only while the repository stays public. Written down beside the finding, with the transcript, so the next person re-checks rather than re-derives.

Not done, and why

Everything else is an Ansible/IaC repo change, and this issue is explicit that it's blocked:

#4 must actually work first. IaC would consume :X.Y.Z, and only release.yml produces those tags — the workflow that has never run once.

So the remaining criteria — a tag producing :X.Y.Z plus a Forgejo release, a role deploying to a prod host, data/ surviving a redeploy, the play waiting on /api/v1/health, rollback as one variable — are all downstream of #4 and outside this repository. I could write an Ansible role speculatively, but it would consume tags that don't exist and couldn't be run, which is worse than not writing it.

Suggested order: verify #4 by cutting a real tag and confirming :X.Y.Z lands in the registry with a Forgejo release beside it. Everything here unblocks the moment that's true.

The three in-repo items are done in #139. **Leaving this open** — the substance is elsewhere and blocked. ## Done - `docs/setup.md` §7 and `docker-compose.registry.yml` now say plainly that they are the **dev-host and self-hoster path**, not the production runbook, with a pointer that production is deployed from the IaC repo and the registry is the boundary. - **The registry question is answered by asking it**, not assuming: ``` GET /v2/ -> 401 Www-Authenticate: Bearer realm=".../v2/token", service="container_registry" GET /v2/token?...&scope=repository:rbrooks/iris-wled:pull -> 200, token issued anonymously GET /v2/rbrooks/iris-wled/manifests/main (with that token) -> 200 ``` **No authentication is required.** The 401 is the standard registry challenge, not a refusal — an anonymous client gets a token that grants `pull`, because the repo is public. So **prod needs no pull secret and no read-only token**, which removes one secret from the IaC side entirely. `docker login` is optional rather than the required step §7 presented it as. That holds only while the repository stays public. Written down beside the finding, with the transcript, so the next person re-checks rather than re-derives. ## Not done, and why Everything else is an **Ansible/IaC repo** change, and this issue is explicit that it's blocked: > **#4 must actually work first.** IaC would consume `:X.Y.Z`, and only `release.yml` produces those tags — the workflow that has never run once. So the remaining criteria — a tag producing `:X.Y.Z` plus a Forgejo release, a role deploying to a prod host, `data/` surviving a redeploy, the play waiting on `/api/v1/health`, rollback as one variable — are all downstream of #4 and outside this repository. I could write an Ansible role speculatively, but it would consume tags that don't exist and couldn't be run, which is worse than not writing it. **Suggested order:** verify #4 by cutting a real tag and confirming `:X.Y.Z` lands in the registry with a Forgejo release beside it. Everything here unblocks the moment that's true.
Author
Contributor

Reopened — merging #139 auto-closed this, which it should not have. The PR said so explicitly ("Does not close it"), so Forgejo picked the reference up from somewhere else, probably the branch name docs/104-deploy-boundary or the (#104, partial) in the squash commit subject.

Nothing has changed about the state: the three in-repo items are done, and everything that gives this issue its title — an IaC role deploying a pinned tag to a prod host, with its own secrets, a persistent data/ volume and a health-checked rollout — is still outstanding, still in the Ansible repo, and still blocked on #4.

Worth remembering for the next partial: reference the issue in prose only, or Forgejo will close it out from under you.

Reopened — merging #139 auto-closed this, which it should not have. The PR said so explicitly ("**Does not close it**"), so Forgejo picked the reference up from somewhere else, probably the branch name `docs/104-deploy-boundary` or the `(#104, partial)` in the squash commit subject. Nothing has changed about the state: the three in-repo items are done, and everything that gives this issue its title — an IaC role deploying a pinned tag to a prod host, with its own secrets, a persistent `data/` volume and a health-checked rollout — is still outstanding, still in the Ansible repo, and still blocked on #4. Worth remembering for the next partial: reference the issue in prose only, or Forgejo will close it out from under you.
Author
Contributor

No longer blocked — the "Blocked on #4" section in the body above is stale

That section says release.yml "has never run once". It has. #4 was proven by a v0.0.1-rc1 dry run on 2026-09-01 and closed, and the real v1.0.0 release has since exercised the parts the RC could not. Verified against the registry and the release API just now, not read off the workflow:

registry tags:  1.0.0    1.0    latest    main
release:        id 476, tag_name v1.0.0, draft false, prerelease false
release body:   16218 chars, byte-identical to the ## [1.0.0] CHANGELOG section

I repeated the "blocked on #4" claim in an earlier comment here, taking it from this issue's body rather than checking. That was wrong, and it is the reason this note exists — the stale text has now misled at least once.

What that changes

:X.Y.Z exists today. An IaC role can pin git.rhoving.com/rbrooks/iris-wled:1.0.0 immediately; there is nothing left to wait for on the build side. The recommendation in the body — pin an immutable tag, never :latest — is directly actionable, and :latest now genuinely moves, so the warning is live rather than theoretical.

No pull secret is needed. From #139: the registry issues an anonymous token granting pull on this repository, and the manifest fetch returns 200. That removes a secret from the IaC side entirely, and holds as long as the repo stays public.

Remaining work, unchanged in substance

Everything else is in the Ansible repo and is the real content of this issue:

  • A role that pulls a pinned tag and runs the container
  • .env and secrets managed there — SESSION_SECRET, OIDC_*, AI_API_KEY, CALENDARIFIC_KEY, LATITUDE/LONGITUDE, WLED_HOST
  • A persistent volume for data/, covering the SQLite DB and data/backups/ — the one piece whose loss is unrecoverable
  • The play waits on GET /api/v1/health rather than assuming docker compose up -d worked
  • Snapshot data/ before pulling a new tag, since an image rollback across a migration is not automatically reversible
  • Rollback is one variable and a re-run

The three in-repo items are done (#139). This stays open for the IaC half.

One choice worth making before writing the role: whether prod pins 1.0.0 — which is what exists and is 23 issues behind main — or waits for a v1.1.0 tag covering the milestone that just completed. Pinning 1.0.0 now and bumping later is the lower-risk order, and it exercises the role against a tag that is already known good.

## No longer blocked — the "Blocked on #4" section in the body above is stale That section says `release.yml` "has never run once". It has. #4 was proven by a `v0.0.1-rc1` dry run on 2026-09-01 and closed, and the real `v1.0.0` release has since exercised the parts the RC could not. Verified against the registry and the release API just now, not read off the workflow: ``` registry tags: 1.0.0 1.0 latest main release: id 476, tag_name v1.0.0, draft false, prerelease false release body: 16218 chars, byte-identical to the ## [1.0.0] CHANGELOG section ``` I repeated the "blocked on #4" claim in an earlier comment here, taking it from this issue's body rather than checking. That was wrong, and it is the reason this note exists — the stale text has now misled at least once. ## What that changes **`:X.Y.Z` exists today.** An IaC role can pin `git.rhoving.com/rbrooks/iris-wled:1.0.0` immediately; there is nothing left to wait for on the build side. The recommendation in the body — pin an immutable tag, never `:latest` — is directly actionable, and `:latest` now genuinely moves, so the warning is live rather than theoretical. **No pull secret is needed.** From #139: the registry issues an anonymous token granting `pull` on this repository, and the manifest fetch returns 200. That removes a secret from the IaC side entirely, and holds as long as the repo stays public. ## Remaining work, unchanged in substance Everything else is in the Ansible repo and is the real content of this issue: - [ ] A role that pulls a pinned tag and runs the container - [ ] `.env` and secrets managed there — `SESSION_SECRET`, `OIDC_*`, `AI_API_KEY`, `CALENDARIFIC_KEY`, `LATITUDE`/`LONGITUDE`, `WLED_HOST` - [ ] A persistent volume for `data/`, covering the SQLite DB **and** `data/backups/` — the one piece whose loss is unrecoverable - [ ] The play waits on `GET /api/v1/health` rather than assuming `docker compose up -d` worked - [ ] Snapshot `data/` before pulling a new tag, since an image rollback across a migration is not automatically reversible - [ ] Rollback is one variable and a re-run The three in-repo items are done (#139). This stays open for the IaC half. **One choice worth making before writing the role:** whether prod pins `1.0.0` — which is what exists and is 23 issues behind `main` — or waits for a `v1.1.0` tag covering the milestone that just completed. Pinning `1.0.0` now and bumping later is the lower-risk order, and it exercises the role against a tag that is already known good.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Iris-WLED#104
No description provided.