# Sublarr Release Workflow — 3-Tier (Prod / RC / Beta) Sublarr runs three isolated tiers, all on Cardinal (Unraid, `192.168.178.36`), each its own Docker Compose project. Modelled on the TravStats 3-tier workflow, adapted to Sublarr's media-locality (the library is local to Cardinal). > Supersedes the earlier "RC-Server to provision" draft — the RC tier now exists > and this describes the implemented system (established 2026-07-09). ## Tiers | Tier | Port | Compose project | Path | Image | DB | Media | |---|---|---|---|---|---|---| | **Prod** | 5765 | `sublarr` | `/mnt/user/appdata/sublarr` | `:X.Y.Z` / `:latest` / `:stable` | real | local library (rw) | | **RC** | 5766 | `sublarr-rc` | `/mnt/user/appdata/sublarr-rc` | `:X.Y.Z-rc.N` | **prod mirror** (re-cloned each round) | `/mnt/user/Sublarr-RC-Media` (rw) | | **Beta** | 5767 | `sublarr-beta` | `/mnt/user/appdata/sublarr-beta` | rolling `:beta` (+ `:beta-`) | own seed | `/mnt/user/Sublarr-Beta-Media` (rw) | - **Prod** is only ever deployed **promoted final tags**, never `-rc.N` or `:beta`. - **RC** is a **prod-data mirror**, re-cloned from prod each RC round (see below). - **Beta** is **internal only** (no public tunnel). The `:beta` image on GHCR is the artifact self-host testers pull; the Cardinal Beta instance is a dogfooding box. - Non-prod tiers set `SUBLARR_STATS_ENDPOINT=""` so RC/Beta never ping the usage-stats aggregate. ## Pipeline | Command | Builds | Deploys | Notes | |---|---|---|---| | **`/deploy-beta`** | `master` HEAD → `:beta` (+ `:beta-`), **amd64-only** | Beta :5767 | On-demand bleeding edge. No version bump / tag. `:beta` is GHCR-only. | | **`/deploy`** | `:X.Y.Z-rc.N`, multi-arch | RC :5766 | After deploy, **mirrors prod DB into RC** via `stage-rc-from-prod.sh`. Per-release candidate. | | **`/release`** | push tag `vX.Y.Z` on the tested RC's commit → `release.yml` rebuilds it as `:X.Y.Z` / `:latest` / `:stable` | Prod :5765 | Same commit as the tested RC, built with `VERSION=X.Y.Z` so it reports `X.Y.Z`, not `-rc.N`. | - `:beta` is amd64-only for fast, frequent on-demand builds; RC and final tags stay multi-arch (amd64 + arm64) for Raspberry Pi / Apple-Silicon self-hosters. ## Promotion rebuilds the RC's commit Until 1.17.0 a release retagged the RC image. The bytes were the tested ones, but they carried `SUBLARR_VERSION=X.Y.Z-rc.N`, so every self-hosted install of a final release reported the RC tag (prod hid it with a compose override). Since 1.17.1 the release tag is the promotion: 1. Bump `backend/VERSION`, build and deploy `X.Y.Z-rc.N` as usual (CI `docker-build.yml -f version=X.Y.Z-rc.N` or `~/bin/sublarr-winbuild`). Both push `:sha-` and `:rc-latest` for an RC. 2. When the RC is approved, tag **that commit** `vX.Y.Z` and push the tag. 3. `release.yml` refuses a tag whose `:sha-` image does not report an RC of `X.Y.Z`, runs CI, builds the commit with `VERSION=X.Y.Z` (multi-arch) as `:X.Y.Z` / `:latest` / `:stable`, mirrors those to Docker Hub, and creates the GitHub release from the CHANGELOG section. It never overwrites an existing `:X.Y.Z`. 4. Deploy prod on `:X.Y.Z`, check the health endpoint reports `X.Y.Z`. The final image is the tested commit, not the tested bytes: dependencies are resolved again at build time, so smoke-test prod on `:X.Y.Z` after the deploy (health, version, a search) instead of assuming bit identity. Never push the release tag on a commit that was not deployed as an RC. ## Release gate — before `/release` promotes anything Check these in order. Any "no" blocks the promotion, not the next RC. 1. **Data-changing migrations are declared.** Every migration in this release that deletes or rewrites rows appears in the version's **Upgrade notes** by revision id, with "back up your database before upgrading" above it. Schema-only migrations just get a line. Deriving this from the changelog prose does not count — the operator reads the notes to decide whether to back up. 2. **Each cleanup migration's writer is closed.** If a migration removes bad data, the code that produced it is fixed in the same release. Otherwise the next run recreates it and the migration silently becomes permanent. 3. **The RC ran under real load long enough to be believed.** For releases carrying data-path changes that means a multi-day watch on a full library, not a smoke test — see `~/SUBLARR-WATCH.md` on CT142 for the running one. 4. **The watch is green on its own terms**, i.e. `sublarr-findings.log` has no unresolved `ERNST` line. Health endpoints being up is not the same thing. 5. **The version-stamped claims still hold.** The README's "Known limitations" section and the docs' Scope & limitations page both carry a version ("Current as of X.Y.Z"), and so do the counted claims next to them — number of providers, the version badge. Re-read them against the release and move the stamp; a stamp that is not moved is a claim nobody checked. Found stale twice: the README badge sat at 1.6.5 while 1.9.4 shipped, and the provider count still read 21 when 29 were registered (forgejo #13). ## RC prod-data mirror Each RC round re-clones prod's Postgres into RC so the RC validates against real state: ```bash FORCE=1 bash scripts/stage-rc-from-prod.sh ``` The script stops the RC app, drops+recreates the RC DB, `pg_dump | psql` from prod, and restarts RC. It is **hard-guarded**: it refuses any target but `sublarr-rc-postgres`, and requires `FORCE=1` — it must never run against Prod or Beta. ## Registries - **GHCR** (`ghcr.io/abrechen2/sublarr`) — primary; receives everything (`:beta`, `-rc.N`, finals). - **Docker Hub** (`docker.io/abrechen2/sublarr`) — mirrors final tags + rolling `:rc-latest`. Both workflows copy them with the `DOCKERHUB_TOKEN` secret; without it they warn and skip. A failed mirror never blocks the GitHub release — rerun the "Mirror to Docker Hub" job. `:beta` stays GHCR-only. ## Beta compose (canonical reference) Lives only on Cardinal at `/mnt/user/appdata/sublarr-beta/docker-compose.yml` (no compose files are committed to this repo). It mirrors the RC stack: `sublarr-beta` / `sublarr-beta-postgres` / `sublarr-beta-redis`, port `5767:5765`, `image: ghcr.io/abrechen2/sublarr:beta`, `env_file: sublarr-beta.env`, volumes `/mnt/user/appdata/sublarr-beta/config:/config` + `/mnt/user/Sublarr-Beta-Media:/media:rw`, named volume `pgdata-beta`. The Beta env is a copy of `sublarr-rc.env` with `SUBLARR_DATABASE_URL` (beta Postgres), `SUBLARR_REDIS_URL` (beta redis), and `SUBLARR_STATS_ENDPOINT=""` overridden. Teardown: `docker compose -p sublarr-beta down -v && rm -rf /mnt/user/appdata/sublarr-beta /mnt/user/Sublarr-Beta-Media`. ## Homepage The homelab Homepage dashboard (CT116, `/opt/homepage/config/services.yaml`) lists all three tiers under the **Sublarr** group: Prod `:5765`, RC `:5766`, Beta `:5767`.