diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index d52c0c4e8..4f45b3a97 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,8 +1,10 @@ name: ci / docker publish # Build the Odysseus image and publish to GHCR. -# push to main -> :latest, :X.Y.Z (curated release; main is fast-forwarded at releases) -# push to dev -> :dev, :X.Y.Z-dev. (rolling dev + an immutable, traceable pin) +# push to main -> :latest, :X.Y.Z, :X.Y.Z- (curated release; main is fast-forwarded at releases; +# :X.Y.Z- is an immutable, traceable prod pin — APP_VERSION may +# not move between builds, so the bare :X.Y.Z tag alone is mutable) +# push to dev -> :dev, :X.Y.Z-dev. (rolling dev + an immutable, traceable pin) # Multi-arch (linux/amd64 + linux/arm64): each arch builds on its own native # runner and pushes by digest, then a merge job stitches the digests into one # manifest list and applies the tags (faster + cleaner than QEMU emulation). @@ -118,6 +120,7 @@ jobs: tags: | type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} type=raw,value=${{ steps.ver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=${{ steps.ver.outputs.version }}-${{ steps.ver.outputs.short }},enable=${{ github.ref == 'refs/heads/main' }} type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }} type=raw,value=${{ steps.ver.outputs.version }}-dev.${{ steps.ver.outputs.short }},enable=${{ github.ref == 'refs/heads/dev' }} - name: Create manifest list + push tags @@ -133,8 +136,16 @@ jobs: IMAGE_NAME: ${{ env.IMAGE_NAME }} - name: Inspect run: | - if [ "$GITHUB_REF" = "refs/heads/main" ]; then ref=latest; else ref=dev; fi - docker buildx imagetools inspect "${REGISTRY}/${IMAGE_NAME}:${ref}" + # main: verify both the mutable :latest and the immutable :X.Y.Z- prod pin + # actually resolved in the registry; dev: verify :dev. + if [ "$GITHUB_REF" = "refs/heads/main" ]; then + refs=("latest" "${{ steps.ver.outputs.version }}-${{ steps.ver.outputs.short }}") + else + refs=("dev") + fi + for ref in "${refs[@]}"; do + docker buildx imagetools inspect "${REGISTRY}/${IMAGE_NAME}:${ref}" + done env: REGISTRY: ${{ env.REGISTRY }} IMAGE_NAME: ${{ env.IMAGE_NAME }} diff --git a/README.md b/README.md index 4cc48f0d4..648d02318 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,14 @@ docker compose up -d --build Open `http://localhost:7000` when the containers are healthy. The first admin password is printed in `docker compose logs odysseus`. +The compose files pull the official multi-arch image `ghcr.io/odysseus-dev/odysseus` (published by CI on every push to `main` and `dev`) and only build locally if the pull fails — so this also works on hosts without a build toolchain, e.g. as a [Portainer](https://www.portainer.io/) stack. + +**Production deployments:** pin the immutable tag instead of `:latest`. `:latest` and bare `:X.Y.Z` tags move on every push to `main`, but `:X.Y.Z-` (e.g. `1.0.2-7c8070f`) always refers to one specific build: + +```bash +ODYSSEUS_IMAGE=ghcr.io/odysseus-dev/odysseus:1.0.2-7c8070f docker compose up -d +``` + Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration live in the [setup guide](docs/setup.md). ## Features diff --git a/docker-compose.gpu-amd.yml b/docker-compose.gpu-amd.yml index 8d0cf1653..92420a013 100644 --- a/docker-compose.gpu-amd.yml +++ b/docker-compose.gpu-amd.yml @@ -12,6 +12,15 @@ # host's numeric render group id when needed. See docker/gpu.amd.yml for details. services: odysseus: + # Official multi-arch GHCR image (linux/amd64 + linux/arm64), published by + # the "ci / docker publish" workflow on every push to main and dev. + # Docker pulls this image when it is reachable, and only falls back to the + # local build below when the pull fails (e.g. no network on the host), so + # hosts without a build toolchain (Portainer stacks, etc.) get the + # registry build. For production, pin an immutable tag via ODYSSEUS_IMAGE + # - e.g. ghcr.io/odysseus-dev/odysseus:1.0.2-7c8070f (X.Y.Z-) - since + # :latest and bare :X.Y.Z tags move on every main push. + image: ${ODYSSEUS_IMAGE:-ghcr.io/odysseus-dev/odysseus:latest} build: . ports: - "${APP_BIND:-127.0.0.1}:${APP_PORT:-7000}:7000" diff --git a/docker-compose.gpu-nvidia.yml b/docker-compose.gpu-nvidia.yml index 69331ffb6..ad29aefd9 100644 --- a/docker-compose.gpu-nvidia.yml +++ b/docker-compose.gpu-nvidia.yml @@ -11,6 +11,15 @@ # for setup details. services: odysseus: + # Official multi-arch GHCR image (linux/amd64 + linux/arm64), published by + # the "ci / docker publish" workflow on every push to main and dev. + # Docker pulls this image when it is reachable, and only falls back to the + # local build below when the pull fails (e.g. no network on the host), so + # hosts without a build toolchain (Portainer stacks, etc.) get the + # registry build. For production, pin an immutable tag via ODYSSEUS_IMAGE + # - e.g. ghcr.io/odysseus-dev/odysseus:1.0.2-7c8070f (X.Y.Z-) - since + # :latest and bare :X.Y.Z tags move on every main push. + image: ${ODYSSEUS_IMAGE:-ghcr.io/odysseus-dev/odysseus:latest} build: . ports: - "${APP_BIND:-127.0.0.1}:${APP_PORT:-7000}:7000" diff --git a/docker-compose.yml b/docker-compose.yml index 708e5df82..331a5a0c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,14 @@ services: odysseus: + # Official multi-arch GHCR image (linux/amd64 + linux/arm64), published by + # the "ci / docker publish" workflow on every push to main and dev. + # Docker pulls this image when it is reachable, and only falls back to the + # local build below when the pull fails (e.g. no network on the host), so + # hosts without a build toolchain (Portainer stacks, etc.) get the + # registry build. For production, pin an immutable tag via ODYSSEUS_IMAGE + # — e.g. ghcr.io/odysseus-dev/odysseus:1.0.2-7c8070f (X.Y.Z-) — since + # :latest and bare :X.Y.Z tags move on every main push. + image: ${ODYSSEUS_IMAGE:-ghcr.io/odysseus-dev/odysseus:latest} build: . ports: - "${APP_BIND:-127.0.0.1}:${APP_PORT:-7000}:7000" diff --git a/docs/setup.md b/docs/setup.md index 523dd41d7..2cab8cee4 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -27,6 +27,24 @@ docker compose up -d --build ``` To include optional extras in the image (PDF viewer, Office extraction; includes AGPL PyMuPDF), build with `docker compose build --build-arg INSTALL_OPTIONAL=true` before `up`. +**Official Docker images.** The compose files reference the official multi-arch image `ghcr.io/odysseus-dev/odysseus`, which CI (the `ci / docker publish` workflow) publishes on every push to `main` and `dev`. When the image is reachable, Compose pulls it instead of building — so the same files work on hosts without a build toolchain (Portainer stacks, Coolify, etc.). `--build` forces a local build regardless. + +Tag scheme: + +| Tag | Meaning | +| --- | --- | +| `:latest`, `:X.Y.Z` | Latest curated build from `main`. **Mutable** — re-pushed on every push to `main`, even without a version bump. | +| `:X.Y.Z-` | Immutable build pin (e.g. `1.0.2-7c8070f`). One tag, one build, forever. **Use this in production.** | +| `:dev`, `:X.Y.Z-dev.` | Rolling `dev` branch builds; the `` form is also immutable. | + +For production, pin the immutable tag by overriding the image in `.env` (or the stack's environment variables): + +```bash +ODYSSEUS_IMAGE=ghcr.io/odysseus-dev/odysseus:1.0.2-7c8070f +``` + +Browse current tags at . (Until this package is made public and linked to the repo by an org owner, pulls fall back to the local build automatically — that fallback is intentional.) + Open `http://localhost:7000` when the containers are healthy. Docker Compose binds the web UI to `127.0.0.1` by default. If the port is taken, set `APP_PORT=7001` in `.env` and recreate the container. Set `APP_BIND=0.0.0.0`