From af5cbb82b8a8317a585548808ed899e232ec1988 Mon Sep 17 00:00:00 2001 From: Bernt Popp Date: Tue, 14 Jul 2026 09:35:00 +0200 Subject: [PATCH 1/2] feat(deploy): pull the released image in the NPM overlay instead of building MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI publishes an attested, digest-addressable image to ghcr.io/berntpopp/stringdb-link on every release. But the deployed compose chain never consumed it. stringdb-link deploys as `docker-compose.yml -f docker-compose.npm.yml`, and the NPM file is a PURE OVERLAY: it declares neither `build:` nor `image:`, so it inherited the base file's `build:` block. Every deploy therefore rebuilt the image from source on the server, and the published, attested image was dead weight. Confirmed on the VPS: stringdb_link_server currently runs `stringdb-link-npm-stringdb-link` — a locally built image, not the GHCR release. The overlay now pulls the release: build: !reset null image: ${STRINGDB_LINK_IMAGE:?...} pull_policy: missing `build: !reset null` is mandatory here. Unlike a self-contained overlay, adding only `image:` would leave the base's `build:` in the merged model and Compose would still consider the service buildable. It fails closed when the pin is unset, matching the contract docker-compose.prod.yml already uses. A digest is required rather than a tag: no `latest` tag is published, and only a digest is immutable. Deliberately unchanged: `container_name` (stringdb_link_server — NPM's proxy_pass resolves the container name, so renaming it would 502 the public host), the Compose project name (stringdb-link-npm — renaming it would rename named volumes), the healthcheck and its start_period, the read-only/cap-drop hardening block, the networks and the `command`. Verified by rendering the actual deployed chain: $ docker compose -f docker/docker-compose.yml -f docker/docker-compose.npm.yml config required variable STRINGDB_LINK_IMAGE is missing a value # fails closed $ STRINGDB_LINK_IMAGE=...@sha256:... docker compose ... config image: ghcr.io/berntpopp/stringdb-link@sha256:... container_name: stringdb_link_server # no `build:` key anywhere in the rendered model — the server cannot build Also adds STRINGDB_LINK_IMAGE to .env.docker.example (it was documented nowhere an operator would copy) and bumps to 4.0.6 to cut a release carrying the fix. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011Zn3DVmcCxo58ujjJjmmfa --- .env.docker.example | 7 +++++++ CHANGELOG.md | 14 ++++++++++++++ docker/docker-compose.npm.yml | 8 ++++++++ pyproject.toml | 2 +- 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.env.docker.example b/.env.docker.example index 4a2d8c5..d3ea8c3 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -6,6 +6,13 @@ # docker-compose.npm.yml interpolates the variables below; pin them so the # server-side `cp .env.docker.example .env.docker` yields a working --env-file. +# ---- Released image (required) ---- +# The NPM deployment pulls the released, attested image from GHCR; it never builds. +# Pin a DIGEST, not a tag: no `latest` tag is published, and only a digest is immutable. +# Resolve it from the release asset `application-release-manifest.json` (.image.digest), +# or: docker buildx imagetools inspect ghcr.io/berntpopp/stringdb-link: +STRINGDB_LINK_IMAGE=ghcr.io/berntpopp/stringdb-link@sha256:0000000000000000000000000000000000000000000000000000000000000000 + # ---- Nginx Proxy Manager (docker-compose.npm.yml) ---- # External NPM network the container joins for reverse-proxy routing. NPM_SHARED_NETWORK_NAME=npm_default diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d23e9d..e6088c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.0.6] - 2026-07-14 + +### Changed + +- **The NPM deployment pulls the released image instead of building from source.** + `docker/docker-compose.npm.yml` is a pure overlay on `docker-compose.yml`, which + defines `build:` — so the deployed chain (`docker-compose.yml -f + docker-compose.npm.yml`) inherited it and the server rebuilt the image on every + deploy, even though CI had already published an attested, digest-addressable image + to GHCR. The overlay now does `build: !reset null` and requires `STRINGDB_LINK_IMAGE` + pinned to a digest, failing closed when it is unset. Nothing else changed: + `container_name` (`stringdb_link_server`, which NPM forwards to), the Compose project + name, the healthcheck, the hardening block, networks and `command` are all preserved. + ## [Unreleased] ## [4.0.5] - 2026-07-13 diff --git a/docker/docker-compose.npm.yml b/docker/docker-compose.npm.yml index 21d33d5..b20980a 100644 --- a/docker/docker-compose.npm.yml +++ b/docker/docker-compose.npm.yml @@ -7,6 +7,14 @@ name: stringdb-link-npm services: stringdb-link: + # Deploy pulls the released, attested image; it never builds from source. + # `!reset` is required: the base compose file defines `build:`, and without this + # the merged model would still be buildable. + # Pin a digest, not a tag: there is no `latest`, and only a digest is immutable. + build: !reset null + image: ${STRINGDB_LINK_IMAGE:?Set STRINGDB_LINK_IMAGE to ghcr.io/berntpopp/stringdb-link@sha256:} + pull_policy: missing + # Production environment optimized for NPM environment: LOGGING__LEVEL: INFO diff --git a/pyproject.toml b/pyproject.toml index 7743777..ebabca2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "stringdb-link" -version = "4.0.5" +version = "4.0.6" description = "High-performance unified API server for STRING protein-protein interaction database with MCP integration" readme = "README.md" license = { text = "MIT" } From 1040fa3670f49519d695031f486c7a8098a04e36 Mon Sep 17 00:00:00 2001 From: Bernt Popp Date: Tue, 14 Jul 2026 09:42:13 +0200 Subject: [PATCH 2/2] chore(release): refresh uv.lock for the version bump `uv.lock` records the project's own version, so bumping [project].version leaves it stale. The container-CI gate runs `uv lock --check` and fails closed on it. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011Zn3DVmcCxo58ujjJjmmfa --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 4a1d81a..db786b7 100644 --- a/uv.lock +++ b/uv.lock @@ -1697,7 +1697,7 @@ wheels = [ [[package]] name = "stringdb-link" -version = "4.0.5" +version = "4.0.6" source = { editable = "." } dependencies = [ { name = "asgi-correlation-id" },