From 2df455784c813e44409d87420ebea9ae73bdeeef Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Thu, 20 Aug 2026 07:49:30 +0200 Subject: [PATCH 1/2] ci(docker): mirror the all-in-one image to Docker Hub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The aio image (#1042) shipped GHCR-only with a TODO to wire the Docker Hub mirror once the Hub repo existed. backend, frontend and the ml sidecar all publish to docker.io/picpeak/*; aio was the only image a Docker Hub user could not pull. merge-aio now follows merge-backend/merge-ml verbatim: DOCKERHUB_ENABLED computed from the repository slug (so forks stay GHCR-only), a gated Docker Hub login, docker.io/picpeak/aio added to the metadata images list, and a Docker Hub manifest inspect. Tag scheme is untouched — the same beta/main/stable/latest/semver tags land in both registries. The build summary drops the "Docker Hub mirror pending" note and lists the aio (and ml) Hub images when the mirror is active. --- .github/workflows/README-DOCKER.md | 2 +- .github/workflows/docker-build.yml | 40 ++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/.github/workflows/README-DOCKER.md b/.github/workflows/README-DOCKER.md index 615a2836..6f431ac9 100644 --- a/.github/workflows/README-DOCKER.md +++ b/.github/workflows/README-DOCKER.md @@ -2,7 +2,7 @@ This GitHub Actions workflow automatically builds and pushes Docker images for the backend, the frontend, and the all-in-one image to GitHub Container Registry (ghcr.io). -The **all-in-one image** (`/aio`, built from `Dockerfile.aio` at the repo root, #1042) bundles the backend and the built frontend into a single container with SQLite as the default engine — one `docker run`, no compose. It follows the same per-arch build → digest-merge → per-version tag scheme as the other two images, is currently GHCR-only (the Docker Hub mirror gets wired later), and every PR additionally runs a `smoke-aio` job that boots the image and asserts the SPA shell, brand-title rendering, immutable asset caching, and the SQLite engine resolution. +The **all-in-one image** (`/aio`, built from `Dockerfile.aio` at the repo root, #1042) bundles the backend and the built frontend into a single container with SQLite as the default engine — one `docker run`, no compose. It follows the same per-arch build → digest-merge → per-version tag scheme as the other two images, is mirrored to Docker Hub (`docker.io/picpeak/aio`) alongside GHCR on the canonical org repo, and every PR additionally runs a `smoke-aio` job that boots the image and asserts the SPA shell, brand-title rendering, immutable asset caching, and the SQLite engine resolution. ## Features diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 3b96d616..3b89b747 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -780,6 +780,15 @@ jobs: run: | repo_lc="${GITHUB_REPOSITORY,,}" echo "AIO_IMAGE_NAME=${repo_lc}/aio" >> "$GITHUB_ENV" + # Mirror manifests to Docker Hub (picpeak/aio) only on the canonical org + # repo, where the DOCKERHUB_* secrets live. Forks (and any other owner) + # fall back to GHCR-only — the Docker Hub image line and login are gated + # on this flag so their builds keep working unchanged. + if [[ "$GITHUB_REPOSITORY" == "PicPeak/picpeak" ]]; then + echo "DOCKERHUB_ENABLED=true" >> "$GITHUB_ENV" + else + echo "DOCKERHUB_ENABLED=false" >> "$GITHUB_ENV" + fi - name: Download digest artifacts uses: actions/download-artifact@v4 @@ -811,16 +820,24 @@ jobs: echo "is_prerelease=false" >> $GITHUB_OUTPUT fi - # Same per-version tag scheme as backend/frontend: every Release Please - # version publishes a matching aio image. GHCR-only for now — the Docker - # Hub mirror (docker.io/picpeak/aio) is wired later once the Hub repo - # exists: add the images line + Docker Hub login exactly like - # merge-backend (#1042). + - name: Log in to Docker Hub + if: env.DOCKERHUB_ENABLED == 'true' + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Same per-version tag scheme as backend/frontend/ml: every Release Please + # version publishes a matching aio image, mirrored to Docker Hub + # (docker.io/picpeak/aio) on the canonical org repo (#1042). - name: Extract metadata for AIO id: meta-aio uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.AIO_IMAGE_NAME }} + images: | + ${{ env.REGISTRY }}/${{ env.AIO_IMAGE_NAME }} + ${{ env.DOCKERHUB_ENABLED == 'true' && 'docker.io/picpeak/aio' || '' }} labels: | org.opencontainers.image.title=PicPeak All-in-one org.opencontainers.image.description=PicPeak backend + frontend in a single container (SQLite default) @@ -857,6 +874,11 @@ jobs: run: | docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.AIO_IMAGE_NAME }}:${{ steps.meta-aio.outputs.version }} + - name: Inspect manifest (Docker Hub) + if: env.DOCKERHUB_ENABLED == 'true' + run: | + docker buildx imagetools inspect docker.io/picpeak/aio:${{ steps.meta-aio.outputs.version }} + # Boot-level verification of the AIO image on every PR: build for the # runner's arch, run it with no DB env (SQLite default), and assert the # things nginx used to guarantee — SPA shell with the brand title rendered, @@ -1388,10 +1410,14 @@ jobs: if [[ "${{ needs.merge-ml.result }}" == "success" ]]; then echo "- ML sidecar (optional): \`${{ env.REGISTRY }}/${{ env.ML_IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY fi - echo "- All-in-one: \`${{ env.REGISTRY }}/${{ env.AIO_IMAGE_NAME }}\` (GHCR only — Docker Hub mirror pending)" >> $GITHUB_STEP_SUMMARY + echo "- All-in-one: \`${{ env.REGISTRY }}/${{ env.AIO_IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY if [[ "$DOCKERHUB_ENABLED" == "true" ]]; then echo "- Backend (Docker Hub): \`docker.io/picpeak/backend\`" >> $GITHUB_STEP_SUMMARY echo "- Frontend (Docker Hub): \`docker.io/picpeak/frontend\`" >> $GITHUB_STEP_SUMMARY + echo "- All-in-one (Docker Hub): \`docker.io/picpeak/aio\`" >> $GITHUB_STEP_SUMMARY + if [[ "${{ needs.merge-ml.result }}" == "success" ]]; then + echo "- ML sidecar (Docker Hub): \`docker.io/picpeak/ml\`" >> $GITHUB_STEP_SUMMARY + fi fi echo "" >> $GITHUB_STEP_SUMMARY From 899c9b34072ca73ddc4391b74ead43ef4157b235 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:24:19 +0200 Subject: [PATCH 2/2] docs(docker): Hub pages for aio + ml, and the image table in the README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit picpeak/ml has an empty Hub overview and picpeak/aio has none at all, while backend and frontend carry hand-written ones — so the two newest images are the two with nothing on their registry page. Adds .github/dockerhub/{aio,ml}.md as the source of those pages and a dockerhub-descriptions job that pushes them on every main merge, so the page cannot drift from the release it describes. backend/frontend stay hand-maintained for now: capturing their current Hub text into files is a prerequisite, not a side effect of this change. README gains a registry table for all four images (both registries share digests and tags), the org-move callout lists the full set, and the feature list finally mentions People in this gallery, which shipped in #1074 without a README line. --- .github/dockerhub/aio.md | 47 +++++++++++++++++++++++++ .github/dockerhub/ml.md | 56 ++++++++++++++++++++++++++++++ .github/workflows/README-DOCKER.md | 8 ++++- .github/workflows/docker-build.yml | 47 +++++++++++++++++++++++++ README.md | 15 ++++++-- 5 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 .github/dockerhub/aio.md create mode 100644 .github/dockerhub/ml.md diff --git a/.github/dockerhub/aio.md b/.github/dockerhub/aio.md new file mode 100644 index 00000000..24ed03e5 --- /dev/null +++ b/.github/dockerhub/aio.md @@ -0,0 +1,47 @@ +# picpeak — All-in-one + +**picpeak** is an open-source, self-hosted **photo-sharing platform for photographers**, with an optional CRM / accounting suite. This image is the **all-in-one** build: the backend, the built web UI and SQLite in **one container, one process** — no compose file, no separate database, no reverse proxy to wire up. + +- 📦 **Source, docs & issues:** https://github.com/PicPeak/picpeak +- 🧩 **Multi-container images:** [`picpeak/backend`](https://hub.docker.com/r/picpeak/backend) + [`picpeak/frontend`](https://hub.docker.com/r/picpeak/frontend) + +## Supported tags +- `latest` / `stable` — latest stable release +- `x.y.z` — a pinned release (**recommended for production**) +- `beta` / `main` — latest build from `main` (may be unstable) +- **Architectures:** `linux/amd64`, `linux/arm64` (x86 and ARM NAS) + +## Quick start + + docker run -d --name picpeak -p 3000:3000 \ + -v picpeak:/data \ + -e JWT_SECRET="$(openssl rand -base64 48)" \ + picpeak/aio:stable + +Then open **http://localhost:3000/admin** and complete the setup wizard. Read the one-time setup token with: + + docker exec picpeak cat /data/db/SETUP_TOKEN + +> 🔗 Share links need to know your address. The image defaults `FRONTEND_URL` to `http://localhost:3000`; pass `-e FRONTEND_URL=https://photos.example.com` (or set the site URL in Settings) before you send a gallery to a client. + +## Ports & volumes +- Container port **3000** (HTTP; put your own TLS terminator in front for public use). +- **One volume: `/data`** — back it up and you have backed up the install. + - `/data/db` — `picpeak.db` and `SETUP_TOKEN` + - `/data/storage` — originals, thumbnails, archives + - `/data/logs`, `/data/backup` + +## External Postgres +SQLite is this image's default, not its only option. Point it at an existing database exactly like the backend image: + + -e DATABASE_CLIENT=pg -e DB_HOST=… -e DB_USER=… -e DB_PASSWORD=… + +## How it differs from the compose stack +- **SQLite takes one writer at a time** — right for a home server, a NAS or a single studio; the compose stack with PostgreSQL is what scales. +- **No Redis** — background jobs run in-process. +- **Face recognition is unavailable** here. It needs the separate [`picpeak/ml`](https://hub.docker.com/r/picpeak/ml) sidecar, and a second image-processing pipeline competing with thumbnailing for one container's CPU would just make the install slow. Run the multi-container deployment for that feature. + +You can move to the full stack later without reinstalling: take a `.picpeak` backup and restore it there. + +## Docs +Volume layout, the external-Postgres variant, TLS, updates and the limits: **https://docs.picpeak.app/deployment/single-container** diff --git a/.github/dockerhub/ml.md b/.github/dockerhub/ml.md new file mode 100644 index 00000000..bb800c7a --- /dev/null +++ b/.github/dockerhub/ml.md @@ -0,0 +1,56 @@ +# picpeak — ML sidecar (face detection) + +**picpeak** is an open-source, self-hosted **photo-sharing platform for photographers**. This image is the **optional face-detection sidecar**: it detects faces in one image and returns a bounding box, five landmarks, quality signals and a 512-d embedding per face. + +**Nothing else.** No database, no volumes, no state, no egress, no model download at runtime. Clustering, person identity, thresholds and every privacy decision live in the picpeak backend, where the data already is — this service forgets each image the moment it answers. + +If you don't run this container, the feature does not exist. + +- 📦 **Source, docs & issues:** https://github.com/PicPeak/picpeak +- 🧩 **Runs with:** [`picpeak/backend`](https://hub.docker.com/r/picpeak/backend) + [`picpeak/frontend`](https://hub.docker.com/r/picpeak/frontend) + +## Supported tags +- `latest` / `stable` — latest stable release +- `x.y.z` — a pinned release (**recommended for production** — keep it on the **same** tag as the backend) +- `beta` / `main` — latest build from `main` (may be unstable) +- **Architectures:** `linux/amd64`, `linux/arm64` + +> The sidecar's API contract is versioned with the backend that calls it, so `PICPEAK_CHANNEL` resolves the same string across all picpeak images. + +## Turning it on +The maintained compose file already contains this service behind a profile — you do not write it by hand: + + docker compose --profile faces up -d + +Then two deliberate actions in the app, neither of which is installing this container: + +1. Enable the **`faces`** feature flag in admin settings. +2. Enable **"Detect people in this gallery"** per event. + +**Nothing in the backend touches this service while the flag is off**, so an install without this container never attempts a connection. + +## Configuration +| | | +|---|---| +| `FACE_ML_TOKEN` | **Required.** The container **refuses to start** without it, so an accidentally published port is never a free face-detection API. Must match the backend's `FACE_ML_TOKEN`. | +| `FACE_ORT_THREADS` | ONNX Runtime threads (default `1`). | + +Port **8000**, no volumes, no published ports needed — the backend reaches it on the compose network. `FACE_ML_URL` defaults to `http://picpeak-ml:8000` (the compose service name), so the standard deployment needs no URL configuration. + +## API +All endpoints except `/health` require the `X-Face-ML-Token` header. + +| | | +|---|---| +| `GET /health` | `{"status": "ok"}` — unauthenticated, used by the healthcheck | +| `GET /info` | `{detector, embedder, model_version, dim}` | +| `POST /faces` | multipart `image` → `{model_version, faces: [...]}` | + +## Models +YuNet (detection) + FaceNet-512 (embedding), **both MIT**, baked into the image and verified by SHA-256 at build time — never downloaded at runtime, so airgapped installs work and a model cannot change under a running deployment. See [`ml/LICENSES.md`](https://github.com/PicPeak/picpeak/blob/main/ml/LICENSES.md) for why these and not InsightFace's non-commercial weights. + +## Not available on the all-in-one image +[`picpeak/aio`](https://hub.docker.com/r/picpeak/aio) sets `PICPEAK_SINGLE_CONTAINER=true` and the backend refuses to enable face recognition there — a second image-processing pipeline competing with thumbnailing for one small container's CPU would not fail loudly, it would just make the install slow. Run the multi-container deployment for this feature. + +## Docs +**https://docs.picpeak.app** · sidecar internals, model conversion and the alignment/threshold contract: [`ml/README.md`](https://github.com/PicPeak/picpeak/blob/main/ml/README.md) diff --git a/.github/workflows/README-DOCKER.md b/.github/workflows/README-DOCKER.md index 6f431ac9..b9cdaf7d 100644 --- a/.github/workflows/README-DOCKER.md +++ b/.github/workflows/README-DOCKER.md @@ -1,6 +1,6 @@ # Docker Build and Push Workflow -This GitHub Actions workflow automatically builds and pushes Docker images for the backend, the frontend, and the all-in-one image to GitHub Container Registry (ghcr.io). +This GitHub Actions workflow automatically builds and pushes Docker images for the backend, the frontend, the all-in-one image and the optional ML sidecar to GitHub Container Registry (ghcr.io). On the canonical org repo every one of them is mirrored to Docker Hub as `docker.io/picpeak/{backend,frontend,aio,ml}`; forks build the same images GHCR-only. The **all-in-one image** (`/aio`, built from `Dockerfile.aio` at the repo root, #1042) bundles the backend and the built frontend into a single container with SQLite as the default engine — one `docker run`, no compose. It follows the same per-arch build → digest-merge → per-version tag scheme as the other two images, is mirrored to Docker Hub (`docker.io/picpeak/aio`) alongside GHCR on the canonical org repo, and every PR additionally runs a `smoke-aio` job that boots the image and asserts the SPA shell, brand-title rendering, immutable asset caching, and the SQLite engine resolution. @@ -12,6 +12,7 @@ The **all-in-one image** (`/aio`, built from `Dockerfile.aio` at the repo - 🔒 **Security scanning** with Trivy vulnerability scanner - 💾 **Build caching** for faster subsequent builds - 📊 **Build summaries** in GitHub Actions UI +- 📝 **Docker Hub pages** for `aio` and `ml` synced from `.github/dockerhub/*.md` on every `main` merge (`dockerhub-descriptions` job). `backend` and `frontend` pages are still hand-maintained in the Hub UI — add `.github/dockerhub/{backend,frontend}.md` with their current text before putting them under the same job. ## Authentication @@ -54,6 +55,11 @@ docker pull ghcr.io/picpeak/picpeak/backend:v1.0.0 # Pull for specific architecture docker pull --platform linux/arm64 ghcr.io/picpeak/picpeak/backend:latest + +# The same images on Docker Hub (identical tags, identical digests) +docker pull picpeak/backend:latest +docker pull picpeak/aio:stable +docker pull picpeak/ml:stable ``` ### Using in Docker Compose diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 3b89b747..070328ad 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1306,6 +1306,53 @@ jobs: run: | docker buildx imagetools inspect docker.io/picpeak/ml:${{ steps.meta-ml.outputs.version }} + # --------------------------------------------------------------------------- + # Docker Hub repository pages + # --------------------------------------------------------------------------- + # The Hub overview for an image is repository metadata, not part of the + # manifest, so pushing tags never updates it. Keep the copy for the two + # newest images in-repo and push it from CI, so a Hub visitor is never + # reading a page that describes a release from six months ago. + # + # Scope: aio and ml only. picpeak/{backend,frontend} still have their pages + # maintained by hand in the Hub UI — bring them under this job by adding + # .github/dockerhub/{backend,frontend}.md with the current text first, + # otherwise this would overwrite them with a near-copy. + # + # Only on `main` pushes for the canonical org repo: descriptions are + # per-repository, not per-tag, so once per merge is exactly enough. + dockerhub-descriptions: + needs: [merge-aio, merge-ml] + if: always() && github.repository == 'PicPeak/picpeak' && github.ref == 'refs/heads/main' && needs.merge-aio.result == 'success' + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Update picpeak/aio description + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: picpeak/aio + short-description: "picpeak all-in-one — self-hosted photo sharing + CRM in a single container (SQLite)." + readme-filepath: .github/dockerhub/aio.md + + # Skipped whenever the sidecar itself was skipped (FACENET_ONNX_URL unset), + # since the Hub repository only exists once something has been pushed to it. + - name: Update picpeak/ml description + if: needs.merge-ml.result == 'success' + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: picpeak/ml + short-description: "picpeak face-detection sidecar — optional and stateless. Pairs with picpeak/backend." + readme-filepath: .github/dockerhub/ml.md + summary: needs: [build-backend, merge-backend, build-frontend, merge-frontend, build-aio, merge-aio, smoke-aio, build-ml, merge-ml] if: always() diff --git a/README.md b/README.md index c121aacc..cfcb2bb4 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ ![PicPeak Gallery Preview](docs/screenshot-gallery.png) > [!IMPORTANT] -> **PicPeak has moved to its own GitHub organization.** Docker images are now at `ghcr.io/picpeak/picpeak/{backend,frontend}` and active development is on `main`. The old `ghcr.io/the-luap/...` path still responds but its tags are **frozen** at 2026-05-27 — if updates never arrive, check your image path first. See **[`docs/migration-to-org.md`](docs/migration-to-org.md)** for the one-line `docker-compose.yml` edit. +> **PicPeak has moved to its own GitHub organization.** Docker images are now at `ghcr.io/picpeak/picpeak/{backend,frontend,aio,ml}` (and on Docker Hub as `picpeak/{backend,frontend,aio,ml}`) and active development is on `main`. The old `ghcr.io/the-luap/...` path still responds but its tags are **frozen** at 2026-05-27 — if updates never arrive, check your image path first. See **[`docs/migration-to-org.md`](docs/migration-to-org.md)** for the one-line `docker-compose.yml` edit. ## Contents @@ -83,6 +83,17 @@ Then open **http://localhost:3000/admin** and read the setup token with `docker The compose stack above is still the right choice for anything busier — SQLite takes one writer at a time, and Postgres is what scales. You can move to it later without reinstalling: take a `.picpeak` backup and restore it into the full stack. See **[Single-container install](https://docs.picpeak.app/deployment/single-container)** for the volume layout, the external-Postgres variant, TLS, and the limits. +### Docker images + +| | GHCR | Docker Hub | +|---|---|---| +| Backend | `ghcr.io/picpeak/picpeak/backend` | [`picpeak/backend`](https://hub.docker.com/r/picpeak/backend) | +| Frontend | `ghcr.io/picpeak/picpeak/frontend` | [`picpeak/frontend`](https://hub.docker.com/r/picpeak/frontend) | +| All-in-one | `ghcr.io/picpeak/picpeak/aio` | [`picpeak/aio`](https://hub.docker.com/r/picpeak/aio) | +| ML sidecar (optional) | `ghcr.io/picpeak/picpeak/ml` | [`picpeak/ml`](https://hub.docker.com/r/picpeak/ml) | + +Both registries get the same digests and the same tags — `stable`/`latest`, a pinned `x.y.z`, and `beta`/`main` for the active development channel — for `linux/amd64` and `linux/arm64`. Keep every image in one install on the **same** tag. + ## 🌟 Why PicPeak? Unlike expensive SaaS solutions, PicPeak gives you: @@ -97,7 +108,7 @@ Unlike expensive SaaS solutions, PicPeak gives you: **For photographers** — drag & drop upload, auto-expiring & password-protected galleries, automated emails, an analytics dashboard, custom themes, a public landing page, and a [Live Slideshow](https://docs.picpeak.app/features/live-slideshow) projector view that auto-picks-up new uploads during live events. -**For clients** — clean mobile-optimized galleries, one-click bulk downloads, smart search, optional guest uploads, and download protection (watermarking + right-click prevention). +**For clients** — clean mobile-optimized galleries, one-click bulk downloads, smart search, **People in this gallery** face grouping (opt-in per gallery, needs the optional [ML sidecar](ml/README.md)), optional guest uploads, and download protection (watermarking + right-click prevention). **Technical** — Docker-ready, automatic thumbnail generation, external media reference mode, smart archiving of expired galleries, S3-compatible [storage backends](https://docs.picpeak.app/features/storage-backends), [webhooks](https://docs.picpeak.app/features/webhooks), and security-first defaults (JWT, rate limiting, CORS).