From a26fdea599476112bd5fff5a459a4544a29325dc Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Tue, 18 Aug 2026 17:39:44 +0200 Subject: [PATCH 01/11] ci: publish the example images to ghcr instead of docker hub Docker Hub `livepeer/` is a curated product namespace and these are examples, so they stay out of it. The workflow defaulted to exactly that namespace whenever the repo variable was unset, so the wrong target was one cleared variable away. GHCR needs no stored credential: the built-in GITHUB_TOKEN publishes, and public packages pull anonymously, so the Docker Hub secrets go away rather than move. template-livepeer-runner and streamdiffusion-livepeer-runner already publish this way. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/images.yml | 23 +++++++++++++++-------- README.md | 13 ++++++++++--- echo/README.md | 2 +- hello-world/README.md | 2 +- realtime-transcription/README.md | 2 +- tiles/README.md | 2 +- 6 files changed, 29 insertions(+), 15 deletions(-) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index b612c71..3e0fc21 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -1,7 +1,12 @@ name: Build example runner images -# This repo ships EXAMPLE runners: images publish as /runner-example-. -# Production runners drop "example" (runner-). +# This repo ships EXAMPLE runners: images publish to the GitHub Container Registry +# as ghcr.io//runner-example-. Production runners drop "example" +# (runner-). +# +# GHCR needs no stored credentials: the built-in GITHUB_TOKEN publishes, and the +# packages are public, so pulling needs none either. Docker Hub is deliberately not +# a second target — see "Images" in the README. # # Runs on main / v* tags / manual dispatch; PRs build only (workflow_dispatch to # pull-test a branch). amd64 only (Livepeer GPU work is NVIDIA/amd64; no CUDA on @@ -32,6 +37,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + packages: write # publishing to ghcr.io strategy: fail-fast: false matrix: @@ -43,8 +49,8 @@ jobs: id: meta uses: docker/metadata-action@v6 with: - # Namespace defaults to livepeer; override via repo variable DOCKERHUB_NAMESPACE. - images: ${{ vars.DOCKERHUB_NAMESPACE || 'livepeer' }}/runner-example-${{ matrix.example }} + # Owner comes from the repo, so a fork publishes under its own namespace. + images: ghcr.io/${{ github.repository_owner }}/runner-example-${{ matrix.example }} tags: | type=raw,value=latest,enable={{is_default_branch}} type=sha,format=short @@ -60,13 +66,14 @@ jobs: uses: docker/setup-buildx-action@v4 # Allowlist push + dispatch — any other event (PRs, incl. pull_request_target) - # is build-only and never touches the Docker Hub secret. - - name: Log in to Docker Hub + # is build-only, and a fork PR's GITHUB_TOKEN cannot write packages anyway. + - name: Log in to GHCR if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' uses: docker/login-action@v4 with: - username: ${{ secrets.CI_DOCKERHUB_USERNAME }} - password: ${{ secrets.CI_DOCKERHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v7 diff --git a/README.md b/README.md index 981e0c3..1db9a54 100644 --- a/README.md +++ b/README.md @@ -127,10 +127,17 @@ The orchestrator and signer services are defined once at the repo root and pulle ### Images -Each example ships a `Dockerfile` and a `compose.yml` that builds it locally. Those with a `Dockerfile` are also hosted on Docker Hub as `runner-example-` (`linux/amd64`), linked from the example's own README. Tags: `latest` (current `main`), `stable` (latest `v*` release), `1.2` / `1.2.3`, `sha-`. +Each example ships a `Dockerfile` and a `compose.yml` that builds it locally. Those with a `Dockerfile` are also published to the GitHub Container Registry as `ghcr.io/livepeer/runner-example-` (`linux/amd64`), linked from the example's own README. Tags: `latest` (current `main`), `stable` (latest `v*` release), `1.2` / `1.2.3`, `sha-`. -> [!NOTE] -> The images publish under `rickstaa/` until they move to the `livepeer` org ([#6](https://github.com/livepeer/runner-app-examples/issues/6)). +The packages are public, so pulling one needs no account and no login: + +```sh +docker pull ghcr.io/livepeer/runner-example-hello-world:latest +``` + +That is the whole reason they are published: an operator can run an example without building it. The compose files still build locally, so nothing here depends on the registry being reachable. + +> [!NOTE] > **Docker Hub is not a second home for these.** GHCR publishes with the built-in `GITHUB_TOKEN`, so there is no credential to store or rotate, and public packages have no anonymous pull limit. Docker Hub `livepeer/` is a product namespace and these are examples, so they stay out of it. Images published under `rickstaa/` before this moved are frozen and should not be used. ### On-chain (paid) setup diff --git a/echo/README.md b/echo/README.md index eeb8a38..80fec16 100644 --- a/echo/README.md +++ b/echo/README.md @@ -34,7 +34,7 @@ Two things follow from that, and they are what this example exists to show: ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, and hosted at [`rickstaa/runner-example-echo`](https://hub.docker.com/r/rickstaa/runner-example-echo) — tags in the [repo README](../README.md#images). +> Built locally by the compose file below, and published at [`ghcr.io/livepeer/runner-example-echo`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-echo) — public, so `docker pull` needs no login; tags in the [repo README](../README.md#images). Start the stack and confirm the runner registered: diff --git a/hello-world/README.md b/hello-world/README.md index 48ccb5c..25d9c88 100644 --- a/hello-world/README.md +++ b/hello-world/README.md @@ -20,7 +20,7 @@ The app is **dynamically registered**: it self-registers with the orchestrator v ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, and hosted at [`rickstaa/runner-example-hello-world`](https://hub.docker.com/r/rickstaa/runner-example-hello-world) — tags in the [repo README](../README.md#images). +> Built locally by the compose file below, and published at [`ghcr.io/livepeer/runner-example-hello-world`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-hello-world) — public, so `docker pull` needs no login; tags in the [repo README](../README.md#images). ```sh docker compose up -d --build diff --git a/realtime-transcription/README.md b/realtime-transcription/README.md index 5f04b73..6d1aeb0 100644 --- a/realtime-transcription/README.md +++ b/realtime-transcription/README.md @@ -61,7 +61,7 @@ ffmpeg -f alsa -i default -ar 16000 -ac 1 -f s16le - \ ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, and hosted at [`rickstaa/runner-example-realtime-transcription`](https://hub.docker.com/r/rickstaa/runner-example-realtime-transcription) — tags in the [repo README](../README.md#images). +> Built locally by the compose file below, and published at [`ghcr.io/livepeer/runner-example-realtime-transcription`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-realtime-transcription) — public, so `docker pull` needs no login; tags in the [repo README](../README.md#images). ```sh docker compose up -d --build # first run downloads the whisper model diff --git a/tiles/README.md b/tiles/README.md index a22d690..a7f5d93 100644 --- a/tiles/README.md +++ b/tiles/README.md @@ -34,7 +34,7 @@ The output image is identical either way. **Capacity changes throughput, not the ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, and hosted at [`rickstaa/runner-example-tiles`](https://hub.docker.com/r/rickstaa/runner-example-tiles) — tags in the [repo README](../README.md#images). +> Built locally by the compose file below, and published at [`ghcr.io/livepeer/runner-example-tiles`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-tiles) — public, so `docker pull` needs no login; tags in the [repo README](../README.md#images). ```sh CAPACITY=1 docker compose up -d --build From 998bc71d04b31fd63c087039a563176831e824e0 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Tue, 18 Aug 2026 17:46:42 +0200 Subject: [PATCH 02/11] docs(ci): keep the push allowlist from reading as redundant The previous wording said a fork PR's token cannot write packages, which is true for pull_request but not for pull_request_target, the case the same sentence names. That invites a reader to delete the `if:` that is actually doing the work. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/images.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 3e0fc21..84f4708 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -66,7 +66,7 @@ jobs: uses: docker/setup-buildx-action@v4 # Allowlist push + dispatch — any other event (PRs, incl. pull_request_target) - # is build-only, and a fork PR's GITHUB_TOKEN cannot write packages anyway. + # is build-only and never gets a package-writing token. - name: Log in to GHCR if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' uses: docker/login-action@v4 From 1c9f7cbe383fe17dc21b8486325164cfa395a459 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Tue, 18 Aug 2026 17:46:43 +0200 Subject: [PATCH 03/11] docs: cut the images section back to what a reader needs The removed sentence restated the paragraph above it, and the registry rationale is maintainer context that already lives in the workflow header and the PR. What survives is the stale-image warning, which is the part that helps someone holding an old `rickstaa/` reference. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1db9a54..6c4f578 100644 --- a/README.md +++ b/README.md @@ -135,9 +135,8 @@ The packages are public, so pulling one needs no account and no login: docker pull ghcr.io/livepeer/runner-example-hello-world:latest ``` -That is the whole reason they are published: an operator can run an example without building it. The compose files still build locally, so nothing here depends on the registry being reachable. - -> [!NOTE] > **Docker Hub is not a second home for these.** GHCR publishes with the built-in `GITHUB_TOKEN`, so there is no credential to store or rotate, and public packages have no anonymous pull limit. Docker Hub `livepeer/` is a product namespace and these are examples, so they stay out of it. Images published under `rickstaa/` before this moved are frozen and should not be used. +> [!NOTE] +> The images previously published under `rickstaa/` on Docker Hub are frozen; use the GHCR ones. ### On-chain (paid) setup From e96a9b44fe504321b88a9051eb98994badecf3bb Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 19 Aug 2026 12:17:02 +0200 Subject: [PATCH 04/11] feat: let an example run the published image instead of building The images CI publishes were reachable only by hand: every compose.yml builds from source and names no image, so there was nothing for a pull to land on. Adding `image:` to the base file looked like the fix but is a trap. Compose's default pull_policy fetches a remote-resolvable image when it is missing locally, even for a service with a build section, so a plain `up -d` on a clean checkout would silently run the published image instead of the contributor's code. `pull_policy: build` avoids that but makes `docker compose pull` skip the service, losing the point. An overlay keeps the base file building from source exactly as before and reaches the registry only when asked, matching how compose.onchain already layers on. CI now parses the merged pair too. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/lint.yml | 4 ++++ README.md | 7 +++++-- echo/compose.image.yml | 12 ++++++++++++ hello-world/compose.image.yml | 12 ++++++++++++ realtime-transcription/compose.image.yml | 12 ++++++++++++ tiles/compose.image.yml | 12 ++++++++++++ 6 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 echo/compose.image.yml create mode 100644 hello-world/compose.image.yml create mode 100644 realtime-transcription/compose.image.yml create mode 100644 tiles/compose.image.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 739c3a0..b492350 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -32,5 +32,9 @@ jobs: example=${dir%/} [ -f "$example/compose.yml" ] || continue docker compose -f "$example/compose.yml" config --quiet || status=1 + if [ -f "$example/compose.image.yml" ]; then + docker compose -f "$example/compose.yml" \ + -f "$example/compose.image.yml" config --quiet || status=1 + fi done exit $status diff --git a/README.md b/README.md index 6c4f578..8953174 100644 --- a/README.md +++ b/README.md @@ -129,12 +129,15 @@ The orchestrator and signer services are defined once at the repo root and pulle Each example ships a `Dockerfile` and a `compose.yml` that builds it locally. Those with a `Dockerfile` are also published to the GitHub Container Registry as `ghcr.io/livepeer/runner-example-` (`linux/amd64`), linked from the example's own README. Tags: `latest` (current `main`), `stable` (latest `v*` release), `1.2` / `1.2.3`, `sha-`. -The packages are public, so pulling one needs no account and no login: +The packages are public, so pulling needs no account and no login. Each example with a `Dockerfile` ships a `compose.image.yml` overlay that swaps the local build for the published image: ```sh -docker pull ghcr.io/livepeer/runner-example-hello-world:latest +cd hello-world +docker compose -f compose.yml -f compose.image.yml up -d ``` +The base `compose.yml` always builds from source, so the registry is reached only when you ask for it by adding the overlay. It stacks with the on-chain one (`-f compose.yml -f compose.onchain.yml -f compose.image.yml`). + > [!NOTE] > The images previously published under `rickstaa/` on Docker Hub are frozen; use the GHCR ones. diff --git a/echo/compose.image.yml b/echo/compose.image.yml new file mode 100644 index 0000000..230aa79 --- /dev/null +++ b/echo/compose.image.yml @@ -0,0 +1,12 @@ +# Run the published image instead of building. Layer it on the offchain base: +# docker compose -f compose.yml -f compose.image.yml up -d +# +# The base file always builds from source, so this overlay is the only way to +# reach the registry; `pull_policy: always` keeps a stale local copy from +# shadowing the published one. Stacks with the on-chain overlay too: +# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d + +services: + app: + image: ghcr.io/livepeer/runner-example-echo:latest + pull_policy: always diff --git a/hello-world/compose.image.yml b/hello-world/compose.image.yml new file mode 100644 index 0000000..7fc831c --- /dev/null +++ b/hello-world/compose.image.yml @@ -0,0 +1,12 @@ +# Run the published image instead of building. Layer it on the offchain base: +# docker compose -f compose.yml -f compose.image.yml up -d +# +# The base file always builds from source, so this overlay is the only way to +# reach the registry; `pull_policy: always` keeps a stale local copy from +# shadowing the published one. Stacks with the on-chain overlay too: +# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d + +services: + app: + image: ghcr.io/livepeer/runner-example-hello-world:latest + pull_policy: always diff --git a/realtime-transcription/compose.image.yml b/realtime-transcription/compose.image.yml new file mode 100644 index 0000000..40fb76e --- /dev/null +++ b/realtime-transcription/compose.image.yml @@ -0,0 +1,12 @@ +# Run the published image instead of building. Layer it on the offchain base: +# docker compose -f compose.yml -f compose.image.yml up -d +# +# The base file always builds from source, so this overlay is the only way to +# reach the registry; `pull_policy: always` keeps a stale local copy from +# shadowing the published one. Stacks with the on-chain overlay too: +# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d + +services: + app: + image: ghcr.io/livepeer/runner-example-realtime-transcription:latest + pull_policy: always diff --git a/tiles/compose.image.yml b/tiles/compose.image.yml new file mode 100644 index 0000000..e54c413 --- /dev/null +++ b/tiles/compose.image.yml @@ -0,0 +1,12 @@ +# Run the published image instead of building. Layer it on the offchain base: +# docker compose -f compose.yml -f compose.image.yml up -d +# +# The base file always builds from source, so this overlay is the only way to +# reach the registry; `pull_policy: always` keeps a stale local copy from +# shadowing the published one. Stacks with the on-chain overlay too: +# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d + +services: + app: + image: ghcr.io/livepeer/runner-example-tiles:latest + pull_policy: always From 4f53771acff7cdbf60926085677e636f0db9573b Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 19 Aug 2026 12:37:35 +0200 Subject: [PATCH 05/11] docs: describe what the base compose file actually does It does not always build from source: with no `image:` key it builds once, then reuses that local image until --build. What is load-bearing is that it never reaches a registry, which is what makes the overlay the only path to the published image. Also drops the note about the frozen `rickstaa/` images. Those were never production, and it was the only thing in the repo pointing readers at a personal namespace. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 3 --- echo/compose.image.yml | 7 ++++--- hello-world/compose.image.yml | 7 ++++--- realtime-transcription/compose.image.yml | 7 ++++--- tiles/compose.image.yml | 7 ++++--- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8953174..7414f41 100644 --- a/README.md +++ b/README.md @@ -138,9 +138,6 @@ docker compose -f compose.yml -f compose.image.yml up -d The base `compose.yml` always builds from source, so the registry is reached only when you ask for it by adding the overlay. It stacks with the on-chain one (`-f compose.yml -f compose.onchain.yml -f compose.image.yml`). -> [!NOTE] -> The images previously published under `rickstaa/` on Docker Hub are frozen; use the GHCR ones. - ### On-chain (paid) setup On-chain runs add a **remote signer** that holds the payer wallet and mints [probabilistic micropayment](https://medium.com/livepeer-blog/a-primer-on-livepeers-probabilistic-micropayments-e16788b29331) tickets; the orchestrator redeems the winning ones. Shared across examples: diff --git a/echo/compose.image.yml b/echo/compose.image.yml index 230aa79..861f7e6 100644 --- a/echo/compose.image.yml +++ b/echo/compose.image.yml @@ -1,9 +1,10 @@ # Run the published image instead of building. Layer it on the offchain base: # docker compose -f compose.yml -f compose.image.yml up -d # -# The base file always builds from source, so this overlay is the only way to -# reach the registry; `pull_policy: always` keeps a stale local copy from -# shadowing the published one. Stacks with the on-chain overlay too: +# The base file names no image, so it never reaches a registry: it builds, then +# reuses that local build until you pass --build. This overlay is the only path +# to the published image, and `pull_policy: always` is what stops a stale local +# copy of the moving `latest` tag from shadowing it. Stacks with on-chain too: # docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d services: diff --git a/hello-world/compose.image.yml b/hello-world/compose.image.yml index 7fc831c..2605a08 100644 --- a/hello-world/compose.image.yml +++ b/hello-world/compose.image.yml @@ -1,9 +1,10 @@ # Run the published image instead of building. Layer it on the offchain base: # docker compose -f compose.yml -f compose.image.yml up -d # -# The base file always builds from source, so this overlay is the only way to -# reach the registry; `pull_policy: always` keeps a stale local copy from -# shadowing the published one. Stacks with the on-chain overlay too: +# The base file names no image, so it never reaches a registry: it builds, then +# reuses that local build until you pass --build. This overlay is the only path +# to the published image, and `pull_policy: always` is what stops a stale local +# copy of the moving `latest` tag from shadowing it. Stacks with on-chain too: # docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d services: diff --git a/realtime-transcription/compose.image.yml b/realtime-transcription/compose.image.yml index 40fb76e..7149268 100644 --- a/realtime-transcription/compose.image.yml +++ b/realtime-transcription/compose.image.yml @@ -1,9 +1,10 @@ # Run the published image instead of building. Layer it on the offchain base: # docker compose -f compose.yml -f compose.image.yml up -d # -# The base file always builds from source, so this overlay is the only way to -# reach the registry; `pull_policy: always` keeps a stale local copy from -# shadowing the published one. Stacks with the on-chain overlay too: +# The base file names no image, so it never reaches a registry: it builds, then +# reuses that local build until you pass --build. This overlay is the only path +# to the published image, and `pull_policy: always` is what stops a stale local +# copy of the moving `latest` tag from shadowing it. Stacks with on-chain too: # docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d services: diff --git a/tiles/compose.image.yml b/tiles/compose.image.yml index e54c413..66c52d3 100644 --- a/tiles/compose.image.yml +++ b/tiles/compose.image.yml @@ -1,9 +1,10 @@ # Run the published image instead of building. Layer it on the offchain base: # docker compose -f compose.yml -f compose.image.yml up -d # -# The base file always builds from source, so this overlay is the only way to -# reach the registry; `pull_policy: always` keeps a stale local copy from -# shadowing the published one. Stacks with the on-chain overlay too: +# The base file names no image, so it never reaches a registry: it builds, then +# reuses that local build until you pass --build. This overlay is the only path +# to the published image, and `pull_policy: always` is what stops a stale local +# copy of the moving `latest` tag from shadowing it. Stacks with on-chain too: # docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d services: From f1a649c09041f7068d34464fa56c2acf2a25b70e Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 19 Aug 2026 12:38:58 +0200 Subject: [PATCH 06/11] docs: give each example the command that runs its published image The repo README carried the same "always builds from source" claim the overlay comments just lost, and the per-example notes named the image without saying how to run it, which was the original gap. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 2 +- echo/README.md | 2 +- hello-world/README.md | 2 +- realtime-transcription/README.md | 2 +- tiles/README.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7414f41..27eb8a1 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ cd hello-world docker compose -f compose.yml -f compose.image.yml up -d ``` -The base `compose.yml` always builds from source, so the registry is reached only when you ask for it by adding the overlay. It stacks with the on-chain one (`-f compose.yml -f compose.onchain.yml -f compose.image.yml`). +The base `compose.yml` names no image, so it never reaches a registry: it builds, then reuses that local build until you pass `--build`. The overlay is the only path to the published image, and it stacks with the on-chain one (`-f compose.yml -f compose.onchain.yml -f compose.image.yml`). ### On-chain (paid) setup diff --git a/echo/README.md b/echo/README.md index 80fec16..1d917f4 100644 --- a/echo/README.md +++ b/echo/README.md @@ -34,7 +34,7 @@ Two things follow from that, and they are what this example exists to show: ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, and published at [`ghcr.io/livepeer/runner-example-echo`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-echo) — public, so `docker pull` needs no login; tags in the [repo README](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-echo`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-echo) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; tags in the [repo README](../README.md#images). Start the stack and confirm the runner registered: diff --git a/hello-world/README.md b/hello-world/README.md index 25d9c88..09c2070 100644 --- a/hello-world/README.md +++ b/hello-world/README.md @@ -20,7 +20,7 @@ The app is **dynamically registered**: it self-registers with the orchestrator v ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, and published at [`ghcr.io/livepeer/runner-example-hello-world`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-hello-world) — public, so `docker pull` needs no login; tags in the [repo README](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-hello-world`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-hello-world) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; tags in the [repo README](../README.md#images). ```sh docker compose up -d --build diff --git a/realtime-transcription/README.md b/realtime-transcription/README.md index 6d1aeb0..c46df30 100644 --- a/realtime-transcription/README.md +++ b/realtime-transcription/README.md @@ -61,7 +61,7 @@ ffmpeg -f alsa -i default -ar 16000 -ac 1 -f s16le - \ ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, and published at [`ghcr.io/livepeer/runner-example-realtime-transcription`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-realtime-transcription) — public, so `docker pull` needs no login; tags in the [repo README](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-realtime-transcription`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-realtime-transcription) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; tags in the [repo README](../README.md#images). ```sh docker compose up -d --build # first run downloads the whisper model diff --git a/tiles/README.md b/tiles/README.md index a7f5d93..9372f90 100644 --- a/tiles/README.md +++ b/tiles/README.md @@ -34,7 +34,7 @@ The output image is identical either way. **Capacity changes throughput, not the ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, and published at [`ghcr.io/livepeer/runner-example-tiles`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-tiles) — public, so `docker pull` needs no login; tags in the [repo README](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-tiles`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-tiles) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; tags in the [repo README](../README.md#images). ```sh CAPACITY=1 docker compose up -d --build From 92d9dda41622fbae5180d0f04d7c69df5acb59d2 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 19 Aug 2026 12:46:21 +0200 Subject: [PATCH 07/11] refactor: run the published image from the base compose file The overlay worked but cost four files, and it left the base file's worst habit intact: with no `image:` key, `up` reuses whatever it built last, so an edit is silently ignored unless you remember --build. Two interpolated fields do the same job in the file that was already there. Unset, the image name is local-only so the registry is never contacted, and `pull_policy: build` rebuilds every `up`, which the layer cache keeps near-instant and which means source edits always take. Set APP_IMAGE and APP_PULL_POLICY=always to run the published image instead. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/lint.yml | 4 ---- README.md | 7 ++++--- echo/README.md | 2 +- echo/compose.image.yml | 13 ------------- echo/compose.yml | 7 +++++++ hello-world/README.md | 2 +- hello-world/compose.image.yml | 13 ------------- hello-world/compose.yml | 7 +++++++ realtime-transcription/README.md | 2 +- realtime-transcription/compose.image.yml | 13 ------------- realtime-transcription/compose.yml | 7 +++++++ tiles/README.md | 2 +- tiles/compose.image.yml | 13 ------------- tiles/compose.yml | 7 +++++++ 14 files changed, 36 insertions(+), 63 deletions(-) delete mode 100644 echo/compose.image.yml delete mode 100644 hello-world/compose.image.yml delete mode 100644 realtime-transcription/compose.image.yml delete mode 100644 tiles/compose.image.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b492350..739c3a0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -32,9 +32,5 @@ jobs: example=${dir%/} [ -f "$example/compose.yml" ] || continue docker compose -f "$example/compose.yml" config --quiet || status=1 - if [ -f "$example/compose.image.yml" ]; then - docker compose -f "$example/compose.yml" \ - -f "$example/compose.image.yml" config --quiet || status=1 - fi done exit $status diff --git a/README.md b/README.md index 27eb8a1..be90c86 100644 --- a/README.md +++ b/README.md @@ -129,14 +129,15 @@ The orchestrator and signer services are defined once at the repo root and pulle Each example ships a `Dockerfile` and a `compose.yml` that builds it locally. Those with a `Dockerfile` are also published to the GitHub Container Registry as `ghcr.io/livepeer/runner-example-` (`linux/amd64`), linked from the example's own README. Tags: `latest` (current `main`), `stable` (latest `v*` release), `1.2` / `1.2.3`, `sha-`. -The packages are public, so pulling needs no account and no login. Each example with a `Dockerfile` ships a `compose.image.yml` overlay that swaps the local build for the published image: +The packages are public, so pulling needs no account and no login. Every `compose.yml` builds from source by default; set two variables to run the published image instead, with no file to edit: ```sh cd hello-world -docker compose -f compose.yml -f compose.image.yml up -d +APP_IMAGE=ghcr.io/livepeer/runner-example-hello-world:latest \ + APP_PULL_POLICY=always docker compose up -d ``` -The base `compose.yml` names no image, so it never reaches a registry: it builds, then reuses that local build until you pass `--build`. The overlay is the only path to the published image, and it stacks with the on-chain one (`-f compose.yml -f compose.onchain.yml -f compose.image.yml`). +Unset, the image name is local-only and the registry is never contacted. This works with the on-chain overlay too, since it only changes how the app image is resolved. ### On-chain (paid) setup diff --git a/echo/README.md b/echo/README.md index 1d917f4..d273ae2 100644 --- a/echo/README.md +++ b/echo/README.md @@ -34,7 +34,7 @@ Two things follow from that, and they are what this example exists to show: ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-echo`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-echo) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; tags in the [repo README](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-echo`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-echo) instead by setting `APP_IMAGE` and `APP_PULL_POLICY=always` — the package is public, so no login; see [Images](../README.md#images). Start the stack and confirm the runner registered: diff --git a/echo/compose.image.yml b/echo/compose.image.yml deleted file mode 100644 index 861f7e6..0000000 --- a/echo/compose.image.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Run the published image instead of building. Layer it on the offchain base: -# docker compose -f compose.yml -f compose.image.yml up -d -# -# The base file names no image, so it never reaches a registry: it builds, then -# reuses that local build until you pass --build. This overlay is the only path -# to the published image, and `pull_policy: always` is what stops a stale local -# copy of the moving `latest` tag from shadowing it. Stacks with on-chain too: -# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d - -services: - app: - image: ghcr.io/livepeer/runner-example-echo:latest - pull_policy: always diff --git a/echo/compose.yml b/echo/compose.yml index cdaa6ef..04649ba 100644 --- a/echo/compose.yml +++ b/echo/compose.yml @@ -13,6 +13,13 @@ services: service: orchestrator app: + # Builds from source by default, rebuilding on every `up` so an edit is + # never silently ignored (the layer cache keeps that cheap). To run the + # published image instead of building: + # APP_IMAGE=ghcr.io/livepeer/runner-example-echo:latest \ + # APP_PULL_POLICY=always docker compose up -d + image: ${APP_IMAGE:-runner-example-echo:local} + pull_policy: ${APP_PULL_POLICY:-build} build: . container_name: example_apps_echo # Wait for the orchestrator's healthcheck so registration doesn't race its boot. diff --git a/hello-world/README.md b/hello-world/README.md index 09c2070..1dc36f0 100644 --- a/hello-world/README.md +++ b/hello-world/README.md @@ -20,7 +20,7 @@ The app is **dynamically registered**: it self-registers with the orchestrator v ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-hello-world`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-hello-world) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; tags in the [repo README](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-hello-world`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-hello-world) instead by setting `APP_IMAGE` and `APP_PULL_POLICY=always` — the package is public, so no login; see [Images](../README.md#images). ```sh docker compose up -d --build diff --git a/hello-world/compose.image.yml b/hello-world/compose.image.yml deleted file mode 100644 index 2605a08..0000000 --- a/hello-world/compose.image.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Run the published image instead of building. Layer it on the offchain base: -# docker compose -f compose.yml -f compose.image.yml up -d -# -# The base file names no image, so it never reaches a registry: it builds, then -# reuses that local build until you pass --build. This overlay is the only path -# to the published image, and `pull_policy: always` is what stops a stale local -# copy of the moving `latest` tag from shadowing it. Stacks with on-chain too: -# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d - -services: - app: - image: ghcr.io/livepeer/runner-example-hello-world:latest - pull_policy: always diff --git a/hello-world/compose.yml b/hello-world/compose.yml index ed3af56..4b55ee1 100644 --- a/hello-world/compose.yml +++ b/hello-world/compose.yml @@ -13,6 +13,13 @@ services: service: orchestrator app: + # Builds from source by default, rebuilding on every `up` so an edit is + # never silently ignored (the layer cache keeps that cheap). To run the + # published image instead of building: + # APP_IMAGE=ghcr.io/livepeer/runner-example-hello-world:latest \ + # APP_PULL_POLICY=always docker compose up -d + image: ${APP_IMAGE:-runner-example-hello-world:local} + pull_policy: ${APP_PULL_POLICY:-build} build: . container_name: example_apps_hello_world # Wait for the orchestrator's healthcheck so registration doesn't race its boot. diff --git a/realtime-transcription/README.md b/realtime-transcription/README.md index c46df30..81b9c1d 100644 --- a/realtime-transcription/README.md +++ b/realtime-transcription/README.md @@ -61,7 +61,7 @@ ffmpeg -f alsa -i default -ar 16000 -ac 1 -f s16le - \ ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-realtime-transcription`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-realtime-transcription) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; tags in the [repo README](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-realtime-transcription`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-realtime-transcription) instead by setting `APP_IMAGE` and `APP_PULL_POLICY=always` — the package is public, so no login; see [Images](../README.md#images). ```sh docker compose up -d --build # first run downloads the whisper model diff --git a/realtime-transcription/compose.image.yml b/realtime-transcription/compose.image.yml deleted file mode 100644 index 7149268..0000000 --- a/realtime-transcription/compose.image.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Run the published image instead of building. Layer it on the offchain base: -# docker compose -f compose.yml -f compose.image.yml up -d -# -# The base file names no image, so it never reaches a registry: it builds, then -# reuses that local build until you pass --build. This overlay is the only path -# to the published image, and `pull_policy: always` is what stops a stale local -# copy of the moving `latest` tag from shadowing it. Stacks with on-chain too: -# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d - -services: - app: - image: ghcr.io/livepeer/runner-example-realtime-transcription:latest - pull_policy: always diff --git a/realtime-transcription/compose.yml b/realtime-transcription/compose.yml index d8a1763..5dd8fcf 100644 --- a/realtime-transcription/compose.yml +++ b/realtime-transcription/compose.yml @@ -14,6 +14,13 @@ services: service: orchestrator app: + # Builds from source by default, rebuilding on every `up` so an edit is + # never silently ignored (the layer cache keeps that cheap). To run the + # published image instead of building: + # APP_IMAGE=ghcr.io/livepeer/runner-example-realtime-transcription:latest \ + # APP_PULL_POLICY=always docker compose up -d + image: ${APP_IMAGE:-runner-example-realtime-transcription:local} + pull_policy: ${APP_PULL_POLICY:-build} build: . container_name: example_apps_realtime_transcription # Wait for the orchestrator's healthcheck so registration doesn't race its boot. diff --git a/tiles/README.md b/tiles/README.md index 9372f90..3e83ab9 100644 --- a/tiles/README.md +++ b/tiles/README.md @@ -34,7 +34,7 @@ The output image is identical either way. **Capacity changes throughput, not the ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-tiles`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-tiles) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; tags in the [repo README](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-tiles`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-tiles) instead by setting `APP_IMAGE` and `APP_PULL_POLICY=always` — the package is public, so no login; see [Images](../README.md#images). ```sh CAPACITY=1 docker compose up -d --build diff --git a/tiles/compose.image.yml b/tiles/compose.image.yml deleted file mode 100644 index 66c52d3..0000000 --- a/tiles/compose.image.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Run the published image instead of building. Layer it on the offchain base: -# docker compose -f compose.yml -f compose.image.yml up -d -# -# The base file names no image, so it never reaches a registry: it builds, then -# reuses that local build until you pass --build. This overlay is the only path -# to the published image, and `pull_policy: always` is what stops a stale local -# copy of the moving `latest` tag from shadowing it. Stacks with on-chain too: -# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d - -services: - app: - image: ghcr.io/livepeer/runner-example-tiles:latest - pull_policy: always diff --git a/tiles/compose.yml b/tiles/compose.yml index 519753b..dae8a01 100644 --- a/tiles/compose.yml +++ b/tiles/compose.yml @@ -15,6 +15,13 @@ services: service: orchestrator app: + # Builds from source by default, rebuilding on every `up` so an edit is + # never silently ignored (the layer cache keeps that cheap). To run the + # published image instead of building: + # APP_IMAGE=ghcr.io/livepeer/runner-example-tiles:latest \ + # APP_PULL_POLICY=always docker compose up -d + image: ${APP_IMAGE:-runner-example-tiles:local} + pull_policy: ${APP_PULL_POLICY:-build} build: . container_name: example_apps_tiles # Wait for the orchestrator's healthcheck so registration doesn't race its boot. From 9105feb648e44275fab153beb66b31755311b192 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 19 Aug 2026 12:51:16 +0200 Subject: [PATCH 08/11] fix: make the registry switch impossible to half-apply Env vars were the smaller change but the image and the pull policy have to move together, and nothing enforced that: setting APP_IMAGE alone built from source and tagged the result as the published image, so you believed you were running the release while running your own build, and your local registry cache was poisoned with a fake `latest`. An overlay cannot be half-applied, so the switch goes back into a file. The base file keeps the part that was worth having on its own: a local-only tag it always rebuilds, so it never reaches a registry and an edit is never silently ignored. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/lint.yml | 4 ++++ README.md | 7 +++---- echo/README.md | 2 +- echo/compose.image.yml | 13 +++++++++++++ echo/compose.yml | 12 +++++------- hello-world/README.md | 2 +- hello-world/compose.image.yml | 13 +++++++++++++ hello-world/compose.yml | 12 +++++------- realtime-transcription/README.md | 2 +- realtime-transcription/compose.image.yml | 13 +++++++++++++ realtime-transcription/compose.yml | 12 +++++------- tiles/README.md | 2 +- tiles/compose.image.yml | 13 +++++++++++++ tiles/compose.yml | 12 +++++------- 14 files changed, 83 insertions(+), 36 deletions(-) create mode 100644 echo/compose.image.yml create mode 100644 hello-world/compose.image.yml create mode 100644 realtime-transcription/compose.image.yml create mode 100644 tiles/compose.image.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 739c3a0..b492350 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -32,5 +32,9 @@ jobs: example=${dir%/} [ -f "$example/compose.yml" ] || continue docker compose -f "$example/compose.yml" config --quiet || status=1 + if [ -f "$example/compose.image.yml" ]; then + docker compose -f "$example/compose.yml" \ + -f "$example/compose.image.yml" config --quiet || status=1 + fi done exit $status diff --git a/README.md b/README.md index be90c86..5d6b452 100644 --- a/README.md +++ b/README.md @@ -129,15 +129,14 @@ The orchestrator and signer services are defined once at the repo root and pulle Each example ships a `Dockerfile` and a `compose.yml` that builds it locally. Those with a `Dockerfile` are also published to the GitHub Container Registry as `ghcr.io/livepeer/runner-example-` (`linux/amd64`), linked from the example's own README. Tags: `latest` (current `main`), `stable` (latest `v*` release), `1.2` / `1.2.3`, `sha-`. -The packages are public, so pulling needs no account and no login. Every `compose.yml` builds from source by default; set two variables to run the published image instead, with no file to edit: +The packages are public, so pulling needs no account and no login. Every `compose.yml` builds from source and tags the result local-only, so it never contacts a registry; each example with a `Dockerfile` ships a `compose.image.yml` overlay that runs the published image instead: ```sh cd hello-world -APP_IMAGE=ghcr.io/livepeer/runner-example-hello-world:latest \ - APP_PULL_POLICY=always docker compose up -d +docker compose -f compose.yml -f compose.image.yml up -d ``` -Unset, the image name is local-only and the registry is never contacted. This works with the on-chain overlay too, since it only changes how the app image is resolved. +The overlay changes the image and forces the pull together, which is why it is a file rather than a pair of variables: naming the image without forcing the pull would build from source and tag the result as the published image. It stacks with the on-chain overlay (`-f compose.yml -f compose.onchain.yml -f compose.image.yml`). ### On-chain (paid) setup diff --git a/echo/README.md b/echo/README.md index d273ae2..1ce4439 100644 --- a/echo/README.md +++ b/echo/README.md @@ -34,7 +34,7 @@ Two things follow from that, and they are what this example exists to show: ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-echo`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-echo) instead by setting `APP_IMAGE` and `APP_PULL_POLICY=always` — the package is public, so no login; see [Images](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-echo`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-echo) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; see [Images](../README.md#images). Start the stack and confirm the runner registered: diff --git a/echo/compose.image.yml b/echo/compose.image.yml new file mode 100644 index 0000000..c52420a --- /dev/null +++ b/echo/compose.image.yml @@ -0,0 +1,13 @@ +# Run the published image instead of building. Layer it on the offchain base: +# docker compose -f compose.yml -f compose.image.yml up -d +# +# Both fields have to change together: naming the image without forcing the pull +# would build from source and tag the result as the published image. An overlay +# cannot be half-applied, which is why this is a file and not two env vars. +# Stacks with the on-chain overlay too: +# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d + +services: + app: + image: ghcr.io/livepeer/runner-example-echo:latest + pull_policy: always diff --git a/echo/compose.yml b/echo/compose.yml index 04649ba..22dfbd0 100644 --- a/echo/compose.yml +++ b/echo/compose.yml @@ -13,13 +13,11 @@ services: service: orchestrator app: - # Builds from source by default, rebuilding on every `up` so an edit is - # never silently ignored (the layer cache keeps that cheap). To run the - # published image instead of building: - # APP_IMAGE=ghcr.io/livepeer/runner-example-echo:latest \ - # APP_PULL_POLICY=always docker compose up -d - image: ${APP_IMAGE:-runner-example-echo:local} - pull_policy: ${APP_PULL_POLICY:-build} + # Always rebuilds, so an edit is never silently ignored; the layer cache + # keeps that near-instant. The tag is local-only, so `up` never contacts a + # registry. Add compose.image.yml to run the published image instead. + image: runner-example-echo:local + pull_policy: build build: . container_name: example_apps_echo # Wait for the orchestrator's healthcheck so registration doesn't race its boot. diff --git a/hello-world/README.md b/hello-world/README.md index 1dc36f0..d363f2e 100644 --- a/hello-world/README.md +++ b/hello-world/README.md @@ -20,7 +20,7 @@ The app is **dynamically registered**: it self-registers with the orchestrator v ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-hello-world`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-hello-world) instead by setting `APP_IMAGE` and `APP_PULL_POLICY=always` — the package is public, so no login; see [Images](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-hello-world`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-hello-world) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; see [Images](../README.md#images). ```sh docker compose up -d --build diff --git a/hello-world/compose.image.yml b/hello-world/compose.image.yml new file mode 100644 index 0000000..be0b73e --- /dev/null +++ b/hello-world/compose.image.yml @@ -0,0 +1,13 @@ +# Run the published image instead of building. Layer it on the offchain base: +# docker compose -f compose.yml -f compose.image.yml up -d +# +# Both fields have to change together: naming the image without forcing the pull +# would build from source and tag the result as the published image. An overlay +# cannot be half-applied, which is why this is a file and not two env vars. +# Stacks with the on-chain overlay too: +# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d + +services: + app: + image: ghcr.io/livepeer/runner-example-hello-world:latest + pull_policy: always diff --git a/hello-world/compose.yml b/hello-world/compose.yml index 4b55ee1..b655220 100644 --- a/hello-world/compose.yml +++ b/hello-world/compose.yml @@ -13,13 +13,11 @@ services: service: orchestrator app: - # Builds from source by default, rebuilding on every `up` so an edit is - # never silently ignored (the layer cache keeps that cheap). To run the - # published image instead of building: - # APP_IMAGE=ghcr.io/livepeer/runner-example-hello-world:latest \ - # APP_PULL_POLICY=always docker compose up -d - image: ${APP_IMAGE:-runner-example-hello-world:local} - pull_policy: ${APP_PULL_POLICY:-build} + # Always rebuilds, so an edit is never silently ignored; the layer cache + # keeps that near-instant. The tag is local-only, so `up` never contacts a + # registry. Add compose.image.yml to run the published image instead. + image: runner-example-hello-world:local + pull_policy: build build: . container_name: example_apps_hello_world # Wait for the orchestrator's healthcheck so registration doesn't race its boot. diff --git a/realtime-transcription/README.md b/realtime-transcription/README.md index 81b9c1d..b8ca85b 100644 --- a/realtime-transcription/README.md +++ b/realtime-transcription/README.md @@ -61,7 +61,7 @@ ffmpeg -f alsa -i default -ar 16000 -ac 1 -f s16le - \ ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-realtime-transcription`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-realtime-transcription) instead by setting `APP_IMAGE` and `APP_PULL_POLICY=always` — the package is public, so no login; see [Images](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-realtime-transcription`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-realtime-transcription) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; see [Images](../README.md#images). ```sh docker compose up -d --build # first run downloads the whisper model diff --git a/realtime-transcription/compose.image.yml b/realtime-transcription/compose.image.yml new file mode 100644 index 0000000..57960c7 --- /dev/null +++ b/realtime-transcription/compose.image.yml @@ -0,0 +1,13 @@ +# Run the published image instead of building. Layer it on the offchain base: +# docker compose -f compose.yml -f compose.image.yml up -d +# +# Both fields have to change together: naming the image without forcing the pull +# would build from source and tag the result as the published image. An overlay +# cannot be half-applied, which is why this is a file and not two env vars. +# Stacks with the on-chain overlay too: +# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d + +services: + app: + image: ghcr.io/livepeer/runner-example-realtime-transcription:latest + pull_policy: always diff --git a/realtime-transcription/compose.yml b/realtime-transcription/compose.yml index 5dd8fcf..223d1a5 100644 --- a/realtime-transcription/compose.yml +++ b/realtime-transcription/compose.yml @@ -14,13 +14,11 @@ services: service: orchestrator app: - # Builds from source by default, rebuilding on every `up` so an edit is - # never silently ignored (the layer cache keeps that cheap). To run the - # published image instead of building: - # APP_IMAGE=ghcr.io/livepeer/runner-example-realtime-transcription:latest \ - # APP_PULL_POLICY=always docker compose up -d - image: ${APP_IMAGE:-runner-example-realtime-transcription:local} - pull_policy: ${APP_PULL_POLICY:-build} + # Always rebuilds, so an edit is never silently ignored; the layer cache + # keeps that near-instant. The tag is local-only, so `up` never contacts a + # registry. Add compose.image.yml to run the published image instead. + image: runner-example-realtime-transcription:local + pull_policy: build build: . container_name: example_apps_realtime_transcription # Wait for the orchestrator's healthcheck so registration doesn't race its boot. diff --git a/tiles/README.md b/tiles/README.md index 3e83ab9..c592a72 100644 --- a/tiles/README.md +++ b/tiles/README.md @@ -34,7 +34,7 @@ The output image is identical either way. **Capacity changes throughput, not the ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-tiles`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-tiles) instead by setting `APP_IMAGE` and `APP_PULL_POLICY=always` — the package is public, so no login; see [Images](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-tiles`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-tiles) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; see [Images](../README.md#images). ```sh CAPACITY=1 docker compose up -d --build diff --git a/tiles/compose.image.yml b/tiles/compose.image.yml new file mode 100644 index 0000000..7773c55 --- /dev/null +++ b/tiles/compose.image.yml @@ -0,0 +1,13 @@ +# Run the published image instead of building. Layer it on the offchain base: +# docker compose -f compose.yml -f compose.image.yml up -d +# +# Both fields have to change together: naming the image without forcing the pull +# would build from source and tag the result as the published image. An overlay +# cannot be half-applied, which is why this is a file and not two env vars. +# Stacks with the on-chain overlay too: +# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d + +services: + app: + image: ghcr.io/livepeer/runner-example-tiles:latest + pull_policy: always diff --git a/tiles/compose.yml b/tiles/compose.yml index dae8a01..3f41b84 100644 --- a/tiles/compose.yml +++ b/tiles/compose.yml @@ -15,13 +15,11 @@ services: service: orchestrator app: - # Builds from source by default, rebuilding on every `up` so an edit is - # never silently ignored (the layer cache keeps that cheap). To run the - # published image instead of building: - # APP_IMAGE=ghcr.io/livepeer/runner-example-tiles:latest \ - # APP_PULL_POLICY=always docker compose up -d - image: ${APP_IMAGE:-runner-example-tiles:local} - pull_policy: ${APP_PULL_POLICY:-build} + # Always rebuilds, so an edit is never silently ignored; the layer cache + # keeps that near-instant. The tag is local-only, so `up` never contacts a + # registry. Add compose.image.yml to run the published image instead. + image: runner-example-tiles:local + pull_policy: build build: . container_name: example_apps_tiles # Wait for the orchestrator's healthcheck so registration doesn't race its boot. From dfa113c220dd19782e05b1f9e9c106c87bdd14ea Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 19 Aug 2026 12:56:23 +0200 Subject: [PATCH 09/11] refactor: run the published image with a flag, not an overlay `docker compose up --pull always` overrides `pull_policy` from the file, so the switch needs no second compose file and no env var pair that can drift apart. `--build` and `--pull always` are then a symmetric pair a reader can find in `--help`. The file keeps `pull_policy: build`, so a bare `up` still builds and never reaches a registry, and an edit is never silently ignored. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/lint.yml | 4 ---- README.md | 7 ++----- echo/README.md | 2 +- echo/compose.image.yml | 13 ------------- echo/compose.yml | 9 +++++---- hello-world/README.md | 2 +- hello-world/compose.image.yml | 13 ------------- hello-world/compose.yml | 9 +++++---- realtime-transcription/README.md | 2 +- realtime-transcription/compose.image.yml | 13 ------------- realtime-transcription/compose.yml | 9 +++++---- tiles/README.md | 2 +- tiles/compose.image.yml | 13 ------------- tiles/compose.yml | 9 +++++---- 14 files changed, 26 insertions(+), 81 deletions(-) delete mode 100644 echo/compose.image.yml delete mode 100644 hello-world/compose.image.yml delete mode 100644 realtime-transcription/compose.image.yml delete mode 100644 tiles/compose.image.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b492350..739c3a0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -32,9 +32,5 @@ jobs: example=${dir%/} [ -f "$example/compose.yml" ] || continue docker compose -f "$example/compose.yml" config --quiet || status=1 - if [ -f "$example/compose.image.yml" ]; then - docker compose -f "$example/compose.yml" \ - -f "$example/compose.image.yml" config --quiet || status=1 - fi done exit $status diff --git a/README.md b/README.md index 5d6b452..15dfe26 100644 --- a/README.md +++ b/README.md @@ -129,15 +129,12 @@ The orchestrator and signer services are defined once at the repo root and pulle Each example ships a `Dockerfile` and a `compose.yml` that builds it locally. Those with a `Dockerfile` are also published to the GitHub Container Registry as `ghcr.io/livepeer/runner-example-` (`linux/amd64`), linked from the example's own README. Tags: `latest` (current `main`), `stable` (latest `v*` release), `1.2` / `1.2.3`, `sha-`. -The packages are public, so pulling needs no account and no login. Every `compose.yml` builds from source and tags the result local-only, so it never contacts a registry; each example with a `Dockerfile` ships a `compose.image.yml` overlay that runs the published image instead: +The packages are public, so no login is needed. `compose.yml` always builds; one flag runs the published image instead: ```sh -cd hello-world -docker compose -f compose.yml -f compose.image.yml up -d +docker compose up -d --pull always ``` -The overlay changes the image and forces the pull together, which is why it is a file rather than a pair of variables: naming the image without forcing the pull would build from source and tag the result as the published image. It stacks with the on-chain overlay (`-f compose.yml -f compose.onchain.yml -f compose.image.yml`). - ### On-chain (paid) setup On-chain runs add a **remote signer** that holds the payer wallet and mints [probabilistic micropayment](https://medium.com/livepeer-blog/a-primer-on-livepeers-probabilistic-micropayments-e16788b29331) tickets; the orchestrator redeems the winning ones. Shared across examples: diff --git a/echo/README.md b/echo/README.md index 1ce4439..0bb1e56 100644 --- a/echo/README.md +++ b/echo/README.md @@ -34,7 +34,7 @@ Two things follow from that, and they are what this example exists to show: ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-echo`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-echo) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; see [Images](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-echo`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-echo) with `docker compose up -d --pull always` — see [Images](../README.md#images). Start the stack and confirm the runner registered: diff --git a/echo/compose.image.yml b/echo/compose.image.yml deleted file mode 100644 index c52420a..0000000 --- a/echo/compose.image.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Run the published image instead of building. Layer it on the offchain base: -# docker compose -f compose.yml -f compose.image.yml up -d -# -# Both fields have to change together: naming the image without forcing the pull -# would build from source and tag the result as the published image. An overlay -# cannot be half-applied, which is why this is a file and not two env vars. -# Stacks with the on-chain overlay too: -# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d - -services: - app: - image: ghcr.io/livepeer/runner-example-echo:latest - pull_policy: always diff --git a/echo/compose.yml b/echo/compose.yml index 22dfbd0..272607f 100644 --- a/echo/compose.yml +++ b/echo/compose.yml @@ -13,10 +13,11 @@ services: service: orchestrator app: - # Always rebuilds, so an edit is never silently ignored; the layer cache - # keeps that near-instant. The tag is local-only, so `up` never contacts a - # registry. Add compose.image.yml to run the published image instead. - image: runner-example-echo:local + # `pull_policy: build` means `up` always builds and never contacts a + # registry, so an edit is never silently ignored (the layer cache keeps the + # rebuild near-instant). `up --pull always` overrides it to run the + # published image instead. + image: ghcr.io/livepeer/runner-example-echo:latest pull_policy: build build: . container_name: example_apps_echo diff --git a/hello-world/README.md b/hello-world/README.md index d363f2e..ec9844d 100644 --- a/hello-world/README.md +++ b/hello-world/README.md @@ -20,7 +20,7 @@ The app is **dynamically registered**: it self-registers with the orchestrator v ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-hello-world`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-hello-world) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; see [Images](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-hello-world`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-hello-world) with `docker compose up -d --pull always` — see [Images](../README.md#images). ```sh docker compose up -d --build diff --git a/hello-world/compose.image.yml b/hello-world/compose.image.yml deleted file mode 100644 index be0b73e..0000000 --- a/hello-world/compose.image.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Run the published image instead of building. Layer it on the offchain base: -# docker compose -f compose.yml -f compose.image.yml up -d -# -# Both fields have to change together: naming the image without forcing the pull -# would build from source and tag the result as the published image. An overlay -# cannot be half-applied, which is why this is a file and not two env vars. -# Stacks with the on-chain overlay too: -# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d - -services: - app: - image: ghcr.io/livepeer/runner-example-hello-world:latest - pull_policy: always diff --git a/hello-world/compose.yml b/hello-world/compose.yml index b655220..fd83277 100644 --- a/hello-world/compose.yml +++ b/hello-world/compose.yml @@ -13,10 +13,11 @@ services: service: orchestrator app: - # Always rebuilds, so an edit is never silently ignored; the layer cache - # keeps that near-instant. The tag is local-only, so `up` never contacts a - # registry. Add compose.image.yml to run the published image instead. - image: runner-example-hello-world:local + # `pull_policy: build` means `up` always builds and never contacts a + # registry, so an edit is never silently ignored (the layer cache keeps the + # rebuild near-instant). `up --pull always` overrides it to run the + # published image instead. + image: ghcr.io/livepeer/runner-example-hello-world:latest pull_policy: build build: . container_name: example_apps_hello_world diff --git a/realtime-transcription/README.md b/realtime-transcription/README.md index b8ca85b..349034e 100644 --- a/realtime-transcription/README.md +++ b/realtime-transcription/README.md @@ -61,7 +61,7 @@ ffmpeg -f alsa -i default -ar 16000 -ac 1 -f s16le - \ ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-realtime-transcription`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-realtime-transcription) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; see [Images](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-realtime-transcription`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-realtime-transcription) with `docker compose up -d --pull always` — see [Images](../README.md#images). ```sh docker compose up -d --build # first run downloads the whisper model diff --git a/realtime-transcription/compose.image.yml b/realtime-transcription/compose.image.yml deleted file mode 100644 index 57960c7..0000000 --- a/realtime-transcription/compose.image.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Run the published image instead of building. Layer it on the offchain base: -# docker compose -f compose.yml -f compose.image.yml up -d -# -# Both fields have to change together: naming the image without forcing the pull -# would build from source and tag the result as the published image. An overlay -# cannot be half-applied, which is why this is a file and not two env vars. -# Stacks with the on-chain overlay too: -# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d - -services: - app: - image: ghcr.io/livepeer/runner-example-realtime-transcription:latest - pull_policy: always diff --git a/realtime-transcription/compose.yml b/realtime-transcription/compose.yml index 223d1a5..a2d6fd8 100644 --- a/realtime-transcription/compose.yml +++ b/realtime-transcription/compose.yml @@ -14,10 +14,11 @@ services: service: orchestrator app: - # Always rebuilds, so an edit is never silently ignored; the layer cache - # keeps that near-instant. The tag is local-only, so `up` never contacts a - # registry. Add compose.image.yml to run the published image instead. - image: runner-example-realtime-transcription:local + # `pull_policy: build` means `up` always builds and never contacts a + # registry, so an edit is never silently ignored (the layer cache keeps the + # rebuild near-instant). `up --pull always` overrides it to run the + # published image instead. + image: ghcr.io/livepeer/runner-example-realtime-transcription:latest pull_policy: build build: . container_name: example_apps_realtime_transcription diff --git a/tiles/README.md b/tiles/README.md index c592a72..accb94c 100644 --- a/tiles/README.md +++ b/tiles/README.md @@ -34,7 +34,7 @@ The output image is identical either way. **Capacity changes throughput, not the ## Run offchain (free) > [!TIP] -> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-tiles`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-tiles) instead with `-f compose.yml -f compose.image.yml` — the package is public, so no login; see [Images](../README.md#images). +> Built locally by the compose file below, or run the published [`ghcr.io/livepeer/runner-example-tiles`](https://github.com/livepeer/runner-app-examples/pkgs/container/runner-example-tiles) with `docker compose up -d --pull always` — see [Images](../README.md#images). ```sh CAPACITY=1 docker compose up -d --build diff --git a/tiles/compose.image.yml b/tiles/compose.image.yml deleted file mode 100644 index 7773c55..0000000 --- a/tiles/compose.image.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Run the published image instead of building. Layer it on the offchain base: -# docker compose -f compose.yml -f compose.image.yml up -d -# -# Both fields have to change together: naming the image without forcing the pull -# would build from source and tag the result as the published image. An overlay -# cannot be half-applied, which is why this is a file and not two env vars. -# Stacks with the on-chain overlay too: -# docker compose -f compose.yml -f compose.onchain.yml -f compose.image.yml up -d - -services: - app: - image: ghcr.io/livepeer/runner-example-tiles:latest - pull_policy: always diff --git a/tiles/compose.yml b/tiles/compose.yml index 3f41b84..ecaffa7 100644 --- a/tiles/compose.yml +++ b/tiles/compose.yml @@ -15,10 +15,11 @@ services: service: orchestrator app: - # Always rebuilds, so an edit is never silently ignored; the layer cache - # keeps that near-instant. The tag is local-only, so `up` never contacts a - # registry. Add compose.image.yml to run the published image instead. - image: runner-example-tiles:local + # `pull_policy: build` means `up` always builds and never contacts a + # registry, so an edit is never silently ignored (the layer cache keeps the + # rebuild near-instant). `up --pull always` overrides it to run the + # published image instead. + image: ghcr.io/livepeer/runner-example-tiles:latest pull_policy: build build: . container_name: example_apps_tiles From 0121dd348fa72e00908dc7ee188f105764d47a0d Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 19 Aug 2026 13:09:01 +0200 Subject: [PATCH 10/11] docs: shorten the compose comment to the two facts that matter Four lines explaining two was the wrong ratio; what a reader needs is that `up` builds and that `--pull always` does not. Co-Authored-By: Claude Opus 5 (1M context) --- echo/compose.yml | 6 ++---- hello-world/compose.yml | 6 ++---- realtime-transcription/compose.yml | 6 ++---- tiles/compose.yml | 6 ++---- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/echo/compose.yml b/echo/compose.yml index 272607f..2da08ad 100644 --- a/echo/compose.yml +++ b/echo/compose.yml @@ -13,10 +13,8 @@ services: service: orchestrator app: - # `pull_policy: build` means `up` always builds and never contacts a - # registry, so an edit is never silently ignored (the layer cache keeps the - # rebuild near-instant). `up --pull always` overrides it to run the - # published image instead. + # `up` always builds, so an edit is never silently ignored and no registry + # is contacted. `up --pull always` runs the published image instead. image: ghcr.io/livepeer/runner-example-echo:latest pull_policy: build build: . diff --git a/hello-world/compose.yml b/hello-world/compose.yml index fd83277..c378abd 100644 --- a/hello-world/compose.yml +++ b/hello-world/compose.yml @@ -13,10 +13,8 @@ services: service: orchestrator app: - # `pull_policy: build` means `up` always builds and never contacts a - # registry, so an edit is never silently ignored (the layer cache keeps the - # rebuild near-instant). `up --pull always` overrides it to run the - # published image instead. + # `up` always builds, so an edit is never silently ignored and no registry + # is contacted. `up --pull always` runs the published image instead. image: ghcr.io/livepeer/runner-example-hello-world:latest pull_policy: build build: . diff --git a/realtime-transcription/compose.yml b/realtime-transcription/compose.yml index a2d6fd8..231dd9b 100644 --- a/realtime-transcription/compose.yml +++ b/realtime-transcription/compose.yml @@ -14,10 +14,8 @@ services: service: orchestrator app: - # `pull_policy: build` means `up` always builds and never contacts a - # registry, so an edit is never silently ignored (the layer cache keeps the - # rebuild near-instant). `up --pull always` overrides it to run the - # published image instead. + # `up` always builds, so an edit is never silently ignored and no registry + # is contacted. `up --pull always` runs the published image instead. image: ghcr.io/livepeer/runner-example-realtime-transcription:latest pull_policy: build build: . diff --git a/tiles/compose.yml b/tiles/compose.yml index ecaffa7..789dcad 100644 --- a/tiles/compose.yml +++ b/tiles/compose.yml @@ -15,10 +15,8 @@ services: service: orchestrator app: - # `pull_policy: build` means `up` always builds and never contacts a - # registry, so an edit is never silently ignored (the layer cache keeps the - # rebuild near-instant). `up --pull always` overrides it to run the - # published image instead. + # `up` always builds, so an edit is never silently ignored and no registry + # is contacted. `up --pull always` runs the published image instead. image: ghcr.io/livepeer/runner-example-tiles:latest pull_policy: build build: . From d8324148bbbd14092166692df0236c9740e6a7b9 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 19 Aug 2026 13:38:59 +0200 Subject: [PATCH 11/11] docs: cut the compose comment to one line Two lines of rationale for two lines of config; the reasoning is in the PR and the history, what a reader needs here is the two commands. Co-Authored-By: Claude Opus 5 (1M context) --- echo/compose.yml | 3 +-- hello-world/compose.yml | 3 +-- realtime-transcription/compose.yml | 3 +-- tiles/compose.yml | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/echo/compose.yml b/echo/compose.yml index 2da08ad..4e52d6f 100644 --- a/echo/compose.yml +++ b/echo/compose.yml @@ -13,8 +13,7 @@ services: service: orchestrator app: - # `up` always builds, so an edit is never silently ignored and no registry - # is contacted. `up --pull always` runs the published image instead. + # `up` always builds; `--pull always` runs the published image instead. image: ghcr.io/livepeer/runner-example-echo:latest pull_policy: build build: . diff --git a/hello-world/compose.yml b/hello-world/compose.yml index c378abd..7297874 100644 --- a/hello-world/compose.yml +++ b/hello-world/compose.yml @@ -13,8 +13,7 @@ services: service: orchestrator app: - # `up` always builds, so an edit is never silently ignored and no registry - # is contacted. `up --pull always` runs the published image instead. + # `up` always builds; `--pull always` runs the published image instead. image: ghcr.io/livepeer/runner-example-hello-world:latest pull_policy: build build: . diff --git a/realtime-transcription/compose.yml b/realtime-transcription/compose.yml index 231dd9b..a7030e3 100644 --- a/realtime-transcription/compose.yml +++ b/realtime-transcription/compose.yml @@ -14,8 +14,7 @@ services: service: orchestrator app: - # `up` always builds, so an edit is never silently ignored and no registry - # is contacted. `up --pull always` runs the published image instead. + # `up` always builds; `--pull always` runs the published image instead. image: ghcr.io/livepeer/runner-example-realtime-transcription:latest pull_policy: build build: . diff --git a/tiles/compose.yml b/tiles/compose.yml index 789dcad..bae6b7f 100644 --- a/tiles/compose.yml +++ b/tiles/compose.yml @@ -15,8 +15,7 @@ services: service: orchestrator app: - # `up` always builds, so an edit is never silently ignored and no registry - # is contacted. `up --pull always` runs the published image instead. + # `up` always builds; `--pull always` runs the published image instead. image: ghcr.io/livepeer/runner-example-tiles:latest pull_policy: build build: .