Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Docker

on:
push:
tags:
- '@lde/search-api-server@*'
workflow_dispatch:
inputs:
version:
description: 'Published package version to build the image from'
required: true

permissions:
contents: read
packages: write

jobs:
publish-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Determine version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
else
echo "version=${GITHUB_REF_NAME##*@}" >> "$GITHUB_OUTPUT"
fi
# The release run pushes the tag before it publishes to npm, so the
# version this workflow was triggered for may not be installable yet.
- name: Wait for the npm publish
run: |
for attempt in $(seq 1 30); do
if npm view "@lde/search-api-server@${{ steps.version.outputs.version }}" version > /dev/null 2>&1; then
exit 0
fi
sleep 10
done
echo "@lde/search-api-server@${{ steps.version.outputs.version }} never appeared on npm" >&2
exit 1
- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/build-push-action@v6
with:
context: packages/search-api-server
build-args: |
VERSION=${{ steps.version.outputs.version }}
push: true
tags: |
ghcr.io/ldelements/search-api-server:${{ steps.version.outputs.version }}
ghcr.io/ldelements/search-api-server:latest
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ await pipeline.run();
<td><a href="https://www.npmjs.com/package/@lde/search-api-graphql"><img src="https://img.shields.io/npm/v/@lde/search-api-graphql" alt="npm"></a></td>
<td>Engine- and domain-agnostic GraphQL surface for search: builds an executable GraphQL schema from a SearchSchema at runtime and serves it as a framework-agnostic fetch handler with a self-contained playground</td>
</tr>
<tr>
<td><a href="packages/search-api-server">@lde/search-api-server</a></td>
<td><a href="https://www.npmjs.com/package/@lde/search-api-server"><img src="https://img.shields.io/npm/v/@lde/search-api-server" alt="npm"></a></td>
<td>The served search API as a bootable process and prebuilt Docker image: mounts a schema-declaration module, binds the GraphQL handler to a Typesense engine, and serves /graphql plus /health from environment config</td>
</tr>
<tr>
<td><a href="packages/search-pipeline">@lde/search-pipeline</a></td>
<td><a href="https://www.npmjs.com/package/@lde/search-pipeline"><img src="https://img.shields.io/npm/v/@lde/search-pipeline" alt="npm"></a></td>
Expand Down Expand Up @@ -241,6 +246,8 @@ graph TD
docgen
search --> text-normalization
search-api-graphql --> search
search-api-server --> search-api-graphql
search-api-server --> search-typesense
search-typesense --> search
search-typesense --> text-normalization
search-typesense --> pipeline
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 15. Ship the served search API as a Docker image installed from npm

Date: 2026-07-23

## Status

Accepted

Extends [ADR 14 (Serve the search GraphQL API with graphql-yoga)](./0014-serve-the-search-graphql-api-with-graphql-yoga.md).
Implements the image layer (layer 3) of
[#600](https://github.com/ldelements/lde/issues/600).

## Context

The `createSearchGraphQLHandler()` fetch handler (ADR 14) still requires a JS
host to mount it. Turnkey, non-JS and ops-driven deployments – and the
dedicated search-API pod topology #600 recommends – need a prebuilt image that
boots from configuration alone.

Three constraints shaped the design:

1. `@lde/search-api-graphql` deliberately names neither the domain nor the
engine, so a bootable server that binds Typesense cannot live there.
2. A mounted ES module cannot use bare imports (`import '@lde/search'` does not
resolve from a mounted path), so the schema mount cannot be authored against
the library. The SHACL + `search:` source #600 assumed does not exist yet –
schema generation is [#495](https://github.com/ldelements/lde/issues/495)’s
still-open scope.
3. Docker-building the Nx workspace inside the image duplicates the release
pipeline and produces images that do not correspond to published, versioned
artifacts.

## Decision

- **A separate composition package, `@lde/search-api-server`**: environment
config, schema-module loading, and a `node:http` server around the ADR 14
handler bound to `createTypesenseSearchEngine`. The engine-agnostic handler
package stays engine-agnostic.
- **The schema mounts as a plain-data declaration module**: a `.mjs` file
default-exporting `SearchType` declarations, validated at boot by
`searchSchema()` – the exact “declarations built outside TypeScript”
path the runtime validation exists for. Optional functions (`derive`,
`transform`) remain expressible; a serving process never calls them. When
#495 delivers the SHACL + `search:` generator, mounted SHACL becomes a
second source for the same schema.
- **The image installs the published package from npm** (`npm install
@lde/search-api-server@<version>` in a `node:alpine` base) instead of
building the workspace. Each image corresponds one-to-one to a published,
provenance-attested npm version, and the Dockerfile stays a few lines.
- **Publishing is tag-triggered**: the release run’s
`@lde/search-api-server@<version>` tag triggers the Docker workflow, which
waits for the version to appear on npm (the release tags before it
publishes), then pushes `ghcr.io/ldelements/search-api-server:<version>` and
`:latest`.

## Consequences

- Ops deploys get a turnkey `/graphql` + playground from a schema mount and
environment variables; JS hosts keep mounting the handler directly.
- The image cannot serve custom-code GraphQL fields – by design (#600): such
consumers use the handler, or build `FROM` this image.
- Image availability trails the npm publish by up to the workflow’s wait; a
missed publish (e.g. the manual first-version bootstrap) surfaces as a failed
Docker run to re-trigger via `workflow_dispatch`.
- The first image requires the package’s manual npm bootstrap
(see [Releasing a new package](../../CLAUDE.md#releasing-a-new-package)).
56 changes: 38 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions packages/search-api-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# The prebuilt served search API (https://github.com/ldelements/lde/issues/600,
# layer 3): installs the published @lde/search-api-server from npm rather than
# building the workspace, so the image is small and reproducible from public,
# provenance-attested artifacts. Built by .github/workflows/docker.yml on each
# release tag of the package.
FROM node:24-alpine

ARG VERSION=latest
ENV NODE_ENV=production
RUN npm install --global @lde/search-api-server@${VERSION}

USER node
EXPOSE 4000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --quiet --output-document=- http://127.0.0.1:${PORT:-4000}/health || exit 1
CMD ["search-api-server"]
Loading