Skip to main content
The official server image is ghcr.io/b3dmar/3ngram. It contains the MCP and REST server, runs as the non-root node user, and includes the Apache-2.0 license and NOTICE. Postgres, pgvector, and Redis remain separate services; see Self-host for the complete deployment path.

Pull a release

Versioned tags identify a release, while latest follows the newest stable release. Use the published digest below when deployment immutability matters.
To pin a specific release instead, use its full version tag, e.g. ghcr.io/b3dmar/3ngram:1.0.2. Every release publishes a multi-platform OCI index for linux/amd64 and linux/arm64. Docker selects the matching image automatically. To inspect or select a platform explicitly:
Published tags are the full version (1.0.0), major/minor (1.0), latest, and sha-<full-git-sha>.

Pin an immutable digest

The release page records the exact multi-platform digest. Pulling by digest prevents a tag change from altering what is deployed:

Verify provenance

The release workflow attaches a BuildKit SBOM and max-mode provenance to the OCI image. It also publishes GitHub-signed SLSA provenance for the image digest. With GitHub CLI authenticated to GitHub, verify both the repository and the exact signer workflow. Public pulls are anonymous; if your environment requires registry credentials, log in with a token that can read packages before running the command.
The release is blocked when Trivy finds a fixable high or critical vulnerability. Unfixed upstream findings remain visible in the scan output but do not block a release.

Worker image

The background worker publishes its own image, ghcr.io/b3dmar/3ngram-worker, alongside the server image on every release (multi-platform, same tag scheme: full version, major/minor, latest, sha-<full-git-sha>). It is built from apps/worker/Dockerfile with the repo root as build context — the worker needs every workspace dependency’s manifest, lockfile, and sources, not just its own directory. The @3ngram/worker npm package itself stays private; the image is the only published artifact.
The worker has no HTTP surface: no EXPOSE, no PORT, and its HEALTHCHECK is a process-liveness probe (node -e "process.exit(0)") rather than an endpoint check. This is PID-only liveness: a missing REDIS_URL is a hard boot failure, but a Redis outage after startup only logs reconnect warnings while the retry loop spins — the process stays alive and the container stays “healthy” with every job stalled. Monitor Redis and queue throughput separately; the healthcheck cannot see them. It validates the same production environment contract as the server (packages/config/src/env.ts) minus PORT: BASE_URL (https) is still required even though the worker serves no HTTP, along with REDIS_URL, a DATABASE_URL whose username matches RUNTIME_DB_ROLE (default app_user) and whose password is strong (12+ characters, not a shipped dev/example value — weak passwords are rejected at boot), no DATABASE_URL_UNPOOLED, and a non-placeholder LOG_HASH_SALT. OAUTH_JWKS is also required in production even though the worker has no OAuth surface — the shared validation does not yet distinguish the consuming process (tracked in issue #204); supply the server’s value until then. SESSION_CLOSER_ENABLED (default false) gates the session-sweep scheduler and closer job processors — with it off, the scheduler is removed entirely and closer jobs no-op; consolidation, surfacing, and OAuth-client GC run regardless. The worker never runs migrations — there is no migrations stage in its Dockerfile. The server image owns schema management, so self-host has exactly one migrator and two deployables cannot race each other over the schema.