Skip to content

feat: Dockerfile + ghcr publish workflow#16

Open
nycterent wants to merge 4 commits into
CloudLLM-ai:masterfrom
nycterent:feat/dockerfile-and-ghcr-release
Open

feat: Dockerfile + ghcr publish workflow#16
nycterent wants to merge 4 commits into
CloudLLM-ai:masterfrom
nycterent:feat/dockerfile-and-ghcr-release

Conversation

@nycterent

Copy link
Copy Markdown

Summary

Adds a multi-stage Dockerfile for mentisdbd plus a GitHub Actions workflow that publishes versioned + latest images to ghcr.io on every daemon version tag.

Why

Building mentisdbd from source via cargo install takes ~10-15 minutes on a 2-core/4GB cloud VM. A pre-built image lets operators provision in seconds, makes mentisdbd usable in CI smoke tests without burning compile time, and gives downstream packagers a stable reference.

Image design

  • Builder: rust:1.83-bookworm with pkg-config libssl-dev libasound2-dev (alsa-sys + reqwest TLS link). cargo install --locked for reproducibility.
  • Runtime: debian:bookworm-slim with libasound2 libssl3 ca-certificates tini. Non-root mentisdb user (uid 991). /var/lib/mentisdb is the named volume.
  • Default ENV: binds 0.0.0.0 (override with MENTISDB_BIND_HOST=127.0.0.1 for sidecar-proxy patterns), audio features off, RUST_LOG=info.
  • EXPOSE: 9471 (HTTP MCP), 9472 (HTTP REST), 9475 (dashboard).
  • ENTRYPOINT: tini + mentisdbd.

Run

docker run -dt --name mentisdbd \
  -p 9471:9471 -p 9472:9472 -p 9475:9475 \
  -v mentisdb-data:/var/lib/mentisdb \
  ghcr.io/cloudllm-ai/mentisdb:0.9.5

-t (allocate pseudo-TTY) is currently required — mentisdbd 0.9.5 opens /dev/tty for TUI init even when running non-interactively, exiting with ENXIO if no controlling terminal is available. A --headless flag (or graceful TUI fallback when isatty(0)/isatty(1) are false AND /dev/tty open fails) would let the image run without -t. Happy to follow up with a PR for that if maintainers prefer.

Workflow

Triggers on daemon-version tags (PascalCase semver: 0.9.5, v0.9.5) — does NOT collide with the existing pymentisdb-v* PyPI publish trigger. Multi-arch (linux/amd64 + linux/arm64) via Buildx + QEMU. Pushes to ghcr.io/$GITHUB_REPOSITORY_OWNER/mentisdb. Uses gha cache, attests provenance + SBOM.

Test plan

  • CI builds amd64 image successfully on PR
  • CI builds arm64 image successfully on PR
  • On a tag push, image lands at ghcr.io/cloudllm-ai/mentisdb:<version> and :latest
  • docker run -dt --rm ghcr.io/cloudllm-ai/mentisdb:<version> starts the daemon and listens on 9471/9472/9475
  • curl http://localhost:9472/v1/agents -X POST -d '{}' -H 'Content-Type: application/json' returns 200

Notes

  • I'm running mentisdbd in production and rebuilt the host VM 7 times in 4 hours iterating cloud-init scripts. A docker image would have saved ~70 min of compile time across those iterations.
  • Hadolint flags two DL3008 warnings (unpinned apt versions). Left them unpinned because most upstream Dockerfiles do — pinning every distro package version creates maintenance churn that doesn't pay off in practice. Happy to pin if you prefer.

Adds a multi-stage Dockerfile that builds mentisdbd in a rust:1.83-bookworm
builder stage and ships it on debian:bookworm-slim. Runtime image includes
libasound2 (alsa-sys link), libssl3, ca-certificates, and tini for clean
PID 1 / signal handling. Runs as a non-root `mentisdb` user (uid/gid 991)
with `/var/lib/mentisdb` as the data volume.

Default env exposes the daemon's MCP (9471), REST (9472), and dashboard
(9475) on 0.0.0.0 inside the container; bind to 127.0.0.1 by setting
`MENTISDB_BIND_HOST` if you want a sidecar reverse-proxy pattern.

The `.github/workflows/docker-publish.yml` workflow builds multi-arch
(linux/amd64 + linux/arm64) images via Buildx + QEMU and pushes to
`ghcr.io/<repo>/mentisdb:<version>` plus `:latest` on every daemon
version tag (PascalCase semver) or via workflow_dispatch. Cache via
GitHub Actions cache for fast iteration. Provenance + SBOM attestation
enabled.

Run with `docker run -dt ...`. The `-t` flag is currently required
because mentisdbd opens `/dev/tty` for TUI init even when run
non-interactively; a `--headless` flag upstream would remove this
requirement.

Why: building mentisdbd from source via `cargo install` takes ~10-15
minutes on a 2-core/4GB cloud VM (cold cargo cache). A pre-built
image lets operators provision in seconds, makes mentisdbd usable
in CI smoke tests / integration suites without burning compile time,
and gives a stable reference for downstream packaging.

Closes-Hint: I'm running mentisdbd in production and have rebuilt the
host VM 7 times in the last 4 hours iterating on cloud-init scripts.
A docker image would have saved ~70 minutes of compile time.
Copilot AI review requested due to automatic review settings April 27, 2026 11:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class containerization for mentisdbd and automated publishing of multi-arch images to GitHub Container Registry on version tag pushes, enabling much faster operator/CI startup versus compiling from source.

Changes:

  • Introduces a multi-stage Dockerfile to build mentisdbd and run it as a non-root user on debian:bookworm-slim.
  • Adds a GitHub Actions workflow to build/push linux/amd64 + linux/arm64 images to GHCR with :latest and version tags.
  • Adds a .dockerignore to keep Docker build context small.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
Dockerfile Multi-stage build (Rust builder → Debian runtime) with non-root runtime defaults for mentisdbd.
.github/workflows/docker-publish.yml Tag-triggered buildx workflow to publish versioned + latest images to GHCR.
.dockerignore Excludes large/non-essential paths from the Docker build context to speed builds.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/docker-publish.yml Outdated
Comment on lines +29 to +42
- name: Checkout
uses: actions/checkout@v4

- name: Resolve version
id: ver
run: |
if [ -n "${{ inputs.version }}" ]; then
echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
else
# Strip leading v from tag refs like refs/tags/v0.9.5
ref="${{ github.ref_name }}"
echo "version=${ref#v}" >> "$GITHUB_OUTPUT"
fi

Comment on lines +3 to +15
on:
push:
tags:
# Only daemon-version tags (PascalCase semver). Excludes pymentisdb-v*.
- '[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch:
inputs:
version:
description: 'Version tag to publish (e.g. 0.9.5)'
required: true
type: string

Comment thread .dockerignore Outdated
README.md
ROADMAP.md
ENGINEERING_PIPELINE.md
MENTISDB_SKILL.md
Comment thread .github/workflows/docker-publish.yml Outdated
Comment on lines +7 to +8
- '[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+'
Comment thread .github/workflows/docker-publish.yml Outdated

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/mentisdb
…r edition2024

Six issues found:

1. Bot CloudLLM-ai#3 (CRITICAL): .dockerignore was excluding MENTISDB_SKILL.md but
   src/server.rs and src/bin/mentisdbd.rs embed it via include_str!.
   Removed the entry; documented why it can't be re-added.

2. Bot CloudLLM-ai#5: IMAGE_NAME used `${{ github.repository_owner }}` which can be
   mixed-case (e.g. CloudLLM-ai). Docker/OCI refs require lowercase.
   Compute lowercase image name in a step output.

3. Bot CloudLLM-ai#4: tag patterns used regex syntax (`[0-9]+`). GH Actions tag
   filters are glob, not regex. Switched to `[0-9]*.[0-9]*.[0-9]*` so
   tags actually trigger the workflow.

4. Bot CloudLLM-ai#1: workflow_dispatch with `version=X` was building from default
   branch HEAD, not from the tag X. Added an inputs.ref defaulting to
   refs/tags/<version> so dispatched builds match the tag content.
   Operator can override via inputs.ref.

5. Bot CloudLLM-ai#2: PR description claimed PR validation but workflow only ran
   on tag pushes. Added pull_request trigger that builds amd64 only,
   no push, used as a smoke test on Dockerfile/workflow/source changes.

6. Edition2024 (caught by failing test build on fork): rust:1.83 can't
   parse Cargo.toml of transitive deps that require edition2024 (e.g.
   line-clipping 0.3.7). Bumped builder to rust:1.85-bookworm.
@nycterent

Copy link
Copy Markdown
Author

Pushed fixes for all 5 review comments + a 6th issue caught by my fork's failing test build:

Force-reverification on my fork now in progress; I'll report a green tag-build before recommending merge.

nycterent added a commit to atvirokodosprendimai/ai-pipeline-template that referenced this pull request Apr 27, 2026
Replaces the in-vm cargo install of mentisdbd with a docker pull +
systemd unit wrapping `docker run`. Cuts cloud-init bootstrap from
~10-15 min cargo compile to <1 min image pull + container start.

Variable rename: var.mentisdb_version (crates.io semver) ->
var.mentisdb_image (full container ref including tag). Default points
at the operator's fork build (ghcr.io/nycterent/mentisdb:0.9.5-test4)
while CloudLLM-ai/mentisdb#16 is pending merge. Once upstream PR
lands and tags an image at ghcr.io/cloudllm-ai/mentisdb:0.9.5, swap
the default.

Container internals:
- Runs as uid 991 (mentisdb user baked into image)
- /var/lib/mentisdb mounted from host (uid 991 owned)
- 9471/9472/9475 published to 127.0.0.1 only; nginx host-side
  proxies HTTPS:443 -> 127.0.0.1:9472
- -t allocates pseudo-TTY for /dev/tty probe; image baked-in tini
  handles SIGTERM cleanly

Drops:
- rustup-init pinned download + sha256 verification
- cargo install with libasound2-dev, build-essential, pkg-config,
  libssl-dev compile-time deps
- mentisdb system user creation (lives inside the container now)
- /etc/mentisdb/mentisdbd.env (env vars passed via docker -e)
- script -qfc PTY workaround (-t flag in docker run replaces it)

Keeps:
- nginx + Basic Auth + certbot reverse proxy on the host
- ufw firewall
- shred of /var/lib/cloud/instance/user-data.txt post-htpasswd
- Newline/CRLF rejection on basic_auth_password
@nycterent

Copy link
Copy Markdown
Author

Pushed missing commit (3f88395) — bumps builder image from rust:1.85-bookworm to rust:1.88-bookworm. Local 1.85 attempt failed because darling 0.23.0 requires rustc 1.88 and icu_collections 2.2.0 requires 1.86. PR head was stale at 5bcad47; now matches the verified-working build on my fork.

Verified end-to-end:

  • Multi-arch image built green on fork (linux/amd64 + linux/arm64)
  • docker pull ghcr.io/nycterent/mentisdb:0.9.5-test4 + docker run -dt -p 9472:9472 ...
  • POST /v1/agents returns {"chain_key":"borganism-brain","agents":[]}
  • Production deploy at https://mem.beerpub.dev now runs from this image (cuts cloud-init bootstrap from ~15min cargo compile to ~2min image pull)

Ready for maintainer review.

@gubatron

gubatron commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Hey @nycterent can't believe I hadn't noticed your PR.
Let me get a breather to take a closer look, test it, seems like a good addition.

Thank you so much for all this work, I'll get on it soon.

@gubatron

gubatron commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

TL;DR — Conditional merge. Fix three things first.

The intent and rationale are sound, the workflow is competently written, and the security posture is mostly good.

But the PR was built/tested against a stale fork and contains a fatal build bug plus two smaller gaps.

If those are fixed, this is a clear net positive for the project.

1. Showstopper — wrong binary name (PR is broken on master)

Cargo.toml:100-103 declares:

[[bin]]
name = "mentisdb"
path = "src/bin/mentisdb.rs"
required-features = ["server"]

The PR's Dockerfile runs:
cargo install --path . --locked --bin mentisdbd --root /opt/mentisdb

--bin mentisdbd does not exist on master. We renamed the binary to simply mentisdb (less correct, but also less confusing to people)

cargo install will fail with error: bin target \mentisdbd\ not found.

The "test plan" in the PR description (CI builds green) cannot have been verified against the current master of this repo — the author was running against their own stale fork that still had the mentisdbd` name.

Fix: --bin mentisdb (or just drop --bin since there's only one).

2. The --headless flag already exists — the PR is wrong about needing it as a follow-up

src/bin/mentisdb.rs already has a working --headless flag (lines 936, 958, 1296, 2310, 2497).

The PR description says:

"-t (allocate pseudo-TTY) is currently required — mentisdbd 0.9.5 opens /dev/tty for TUI init even when running non-> interactively... A --headless flag... would let the image run without -t."

That's a stale claim. The right entrypoint is:

ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/mentisdb", "--headless"]

…and the documented docker run becomes plain docker run -d ... (no -t). This is a quality-of-life fix, but it's also a documentation accuracy issue: the PR is explicitly telling the user to do the wrong thing.

3. Tag patterns are too loose ([0-9].[0-9].[0-9]*)

GitHub Actions tag patterns are glob; * matches any character including .. So:

  • [0-9]*.[0-9]*.[0-9]* matches 1.2.3.4, 12.34.56.78.9, 1a.2b.3c, 0.9.5-test4`, etc.
  • It does not collide with pymentisdb-v1.0.0 (which the existing publish.yml handles), since pymentisdb-v* starts with p.

In practice the only realistic collision vector is your own future tags. If you ever tag mentisdb-v0.9.5 or pymentisdb-0.9.5 the workflow will try to build a Docker image from it.

A safer regex-style glob would be [0-9]+.[0-9]+.[0-9]+ but GHA doesn't support +, so the strict glob equivalent is [0-9][0-9]*.[0-9][0-9]*.[0-9][0-9]* (digit + optional more digits).

Not blocking, but worth tightening.

4. Security & Safety, a few gaps

  • libssl3 in the runtime image is unnecessary. Cargo.toml:63 uses reqwest = { features = ["json", "rustls"] } and rustls = { features = ["ring"] }. The runtime doesn't need libssl. fastembed / ort may pull in openssl-sys via aws-lc-sys/ring which is statically linked, so the .so isn't actually needed at runtime. It's a few MB of dead weight. Not a security issue, but it's the kind of thing reviewers should call out.
  • No image scanner step (trivy, grype, or docker/scout-action). For a project publishing to GHCR, a scan-and-fail-on-CRITICAL step in the same workflow is a small addition that catches regressions cheaply.
  • No HEALTHCHECK. The dashboard listens on 9475 and would be a natural probe, but curl isn't in the image. Either add curl for the healthcheck or document that orchestrators should use a TCP probe on 9471/9472/9475. (HEALTHCHECK NONE + publish port is fine too, just be explicit.)
  • No image signing beyond SLSA provenance. Sigstore cosign signing is a nice-to-have for a memory store that may run in security-sensitive contexts.
  • MENTISDB_BIND_HOST=0.0.0.0 as the default is the safe-but-permissive choice and is correctly documented. Good.
  • .dockerignore correctly excludes data/, docs/, target/, lme-benches/, locomo-benches/, examples/, pymentisdb/, tests/, benches/, *.tex, .pdf, WHITEPAPER., build-whitepaper.sh, AGENTS.md, ENGINEERING_PIPELINE.md, ROADMAP.md, 0.8.6-plan.md, README.md. Good. The author explicitly called out the MENTISDB_SKILL.md carve-out (it's include_str!-ed by src/server.rs:111, src/bin/mentisdb.rs:58, tests/server_tests.rs:22). That self-review is a positive signal.

Merge recommendation

Conditional yes.

Please:

  1. Fix the binary name (--bin mentisdb or drop --bin). Without this, the image literally will not build. This is a hard blocker.
  2. Use the existing --headless flag in the ENTRYPOINT so the documented docker run doesn't need -t, and correct the PR description's claim that --headless doesn't exist.
  3. Tighten the tag glob to [0-9][0-9].[0-9][0-9].[0-9][0-9]* (and the v-prefixed variant) to prevent accidental triggers from future oddly-named tags.

Nice-to-haves that I'd accept in a follow-up PR, not block on:

  • Drop libssl3 from runtime (it's not linked dynamically).
  • Add HEALTHCHECK (and a curl static binary) or document the orchestrator probe.
  • Add a trivy/grype scan step.
  • Add a make docker-run / make docker-build target in the existing Makefile.
  • Consider rust-toolchain.toml so the local toolchain, build.yml, and Dockerfile all stay in sync.
    The rationale is correct, the architecture is sound, the security posture is reasonable, and the CI impact is contained. Once the binary-name bug is fixed, this is a clear win for the project — first-class containerization + automated publishing closes a real deployment gap that's currently costing downstream operators an order of magnitude of bootstrap time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants