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
120 changes: 120 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Build and push Docker image to GHCR

# Builds linux/amd64 and linux/arm64 on NATIVE runners (not QEMU) so that
# C-extension packages (cffi, cryptography, tree-sitter) compile correctly on
# each arch. The two platform images are pushed as digests, then merged into
# a single multi-arch manifest.

on:
push:
branches: ["v8"]
release:
types: [published]
workflow_dispatch:

permissions:
contents: read
packages: write # required to push to ghcr.io

jobs:
build:
name: Build ${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm

steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

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

- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}

# Push a platform-specific digest (no tag yet — the merge job tags it).
- name: Build and push digest
id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
push: true
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}

# Pass the digest to the merge job via an artifact.
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- uses: actions/upload-artifact@v4
with:
name: digest-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
path: /tmp/digests/*
retention-days: 1

merge:
name: Merge into multi-arch manifest
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digest-*
merge-multiple: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

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

- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
# latest on every v8 push
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/v8' }}
# semver tags on release (e.g. 0.9.28, 0.9)
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
# short-sha for traceability
type=sha,prefix=sha-

- name: Create and push multi-arch manifest
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)

- name: Inspect final manifest
run: |
docker buildx imagetools inspect ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }}
33 changes: 27 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
# graphify MCP server as a shared HTTP service (issue #1143).
# graphify MCP server as a shared HTTP service (issue #1, #1143).
# Works with both Docker and Podman.
#
# Build: docker build -t graphify .
# Run: docker run -p 8080:8080 -v "$(pwd)/graphify-out:/data" graphify \
# /data/graph.json --transport http --host 0.0.0.0 --api-key "$SECRET"
# Recommended: use the Makefile or docker-compose.yaml instead of bare commands.
# make build # build the image
# make index # index the current directory (uses graphify CLI)
# make up # start the MCP HTTP server
#
# Manual build:
# docker build -t graphify . # Docker
# podman build -t graphify . # Podman
#
# Run MCP HTTP server (default):
# docker run -p 8080:8080 -v "$(pwd)/graphify-out:/data:ro" \
# -e GRAPHIFY_API_KEY="$SECRET" graphify \
# /data/graph.json --transport http --host 0.0.0.0
#
# Code-only index (override entrypoint to graphify CLI):
# docker run --rm --entrypoint graphify \
# -v "$(pwd):/src:ro" -v "$(pwd)/graphify-out:/data/graphify-out" graphify \
# extract /src --code-only --output /data
#
# Builds from source so the image includes the Streamable HTTP transport even
# before it lands on PyPI. The graph.json is mounted at runtime (-v), never
Expand All @@ -13,13 +29,18 @@ WORKDIR /app
COPY . /app

# The [mcp] extra pulls mcp + starlette + uvicorn, which the HTTP transport needs.
RUN pip install --no-cache-dir ".[mcp]"
# Pin mcp<2.0.0: mcp 2.0.0 removed mcp.types.AnyUrl which graphify.serve imports,
# and introduced pyjwt[crypto] -> cryptography (Rust) which crashes with SIGILL on
# some ARM64 container runtimes. mcp 1.x avoids both issues.
RUN pip install --no-cache-dir ".[mcp]" \
&& pip install --no-cache-dir "mcp<2.0.0" \
&& pip install --no-cache-dir "cryptography>=41,<42"

# Run as a non-root user — the server is network-exposed.
RUN useradd --create-home --uid 10001 graphify
USER graphify

EXPOSE 8080

ENTRYPOINT ["python", "-m", "graphify.serve"]
ENTRYPOINT ["graphify-mcp"]
CMD ["/data/graph.json", "--transport", "http", "--host", "0.0.0.0", "--port", "8080"]
84 changes: 84 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Makefile — build and run graphify via Docker or Podman (issue #1)
#
# Auto-detects docker; falls back to podman.
# Override: make RUNTIME=podman build

RUNTIME ?= $(shell command -v docker 2>/dev/null || command -v podman 2>/dev/null)
COMPOSE := $(RUNTIME) compose

# Directory to index (override with: make index SRC=/path/to/project)
SRC ?= $(CURDIR)

# ANSI colors
BOLD := \033[1m
CYAN := \033[36m
GREEN := \033[32m
YELLOW := \033[33m
DIM := \033[2m
RESET := \033[0m

.PHONY: help pull build up down index logs
.DEFAULT_GOAL := help

## Show this help message
help:
@printf "\n$(BOLD)$(CYAN)graphify$(RESET) — containerized knowledge graph for your codebase\n"
@printf "$(DIM)Runtime: $(RUNTIME)$(RESET)\n\n"
@printf "$(BOLD)$(GREEN)Quick start$(RESET)\n"
@printf " $(CYAN)make pull$(RESET) then $(CYAN)make index$(RESET) then $(CYAN)GRAPHIFY_API_KEY=secret make up$(RESET)\n\n"
@printf "$(BOLD)$(GREEN)Targets$(RESET)\n"
@printf " $(BOLD)$(YELLOW)%-10s$(RESET) %s\n" "pull" "Pull pre-built image from GHCR (no local build needed)"
@printf " $(BOLD)$(YELLOW)%-10s$(RESET) %s\n" "build" "Build image locally from source"
@printf " $(BOLD)$(YELLOW)%-10s$(RESET) %s\n" "index" "Index SRC dir into ./graphify-out/graph.json (code-only)"
@printf " $(BOLD)$(YELLOW)%-10s$(RESET) %s\n" "up" "Start the MCP HTTP server (pull or build first)"
@printf " $(BOLD)$(YELLOW)%-10s$(RESET) %s\n" "down" "Stop the MCP HTTP server"
@printf " $(BOLD)$(YELLOW)%-10s$(RESET) %s\n" "logs" "Tail MCP server logs"
@printf "\n$(BOLD)$(GREEN)Variables$(RESET)\n"
@printf " $(CYAN)SRC$(RESET) Directory to index $(DIM)(default: \$$PWD)$(RESET)\n"
@printf " $(CYAN)RUNTIME$(RESET) docker or podman $(DIM)(default: auto-detect)$(RESET)\n"
@printf " $(CYAN)GRAPHIFY_API_KEY$(RESET) Bearer token for MCP server $(DIM)(required for \`make up\`)$(RESET)\n"
@printf " $(CYAN)GRAPHIFY_PORT$(RESET) Host port for MCP server $(DIM)(default: 8080)$(RESET)\n"
@printf " $(CYAN)GRAPHIFY_IMAGE$(RESET) Override GHCR image $(DIM)(default: ghcr.io/slmingol/graphify:latest)$(RESET)\n"
@printf " $(CYAN)ANTHROPIC_API_KEY$(RESET) LLM key for semantic index $(DIM)(optional)$(RESET)\n"
@printf "\n$(BOLD)$(GREEN)Examples$(RESET)\n"
@printf " $(DIM)# Index a different project$(RESET)\n"
@printf " make index SRC=/path/to/project\n\n"
@printf " $(DIM)# Semantic extraction with Claude$(RESET)\n"
@printf " make index ANTHROPIC_API_KEY=sk-...\n\n"
@printf " $(DIM)# Use podman explicitly$(RESET)\n"
@printf " make RUNTIME=podman up\n\n"

## Pull the pre-built image from GHCR (fastest path — no local build needed)
pull:
$(COMPOSE) pull

## Build the graphify image locally from source
build:
$(COMPOSE) build

## Pull from GHCR if available, otherwise build locally, then start the MCP server
## Set GRAPHIFY_API_KEY in your shell before running.
up:
$(COMPOSE) pull mcp 2>/dev/null || $(COMPOSE) build mcp
$(COMPOSE) up mcp

## Stop the MCP HTTP server
down:
$(COMPOSE) down

## Index SRC into ./graphify-out/graph.json (code-only, no API key needed)
## For full semantic extraction pass your key: make index ANTHROPIC_API_KEY=sk-...
## Usage: make index OR make index SRC=/path/to/project
index:
mkdir -p ./graphify-out
$(COMPOSE) --profile cli pull graphify 2>/dev/null || $(COMPOSE) --profile cli build graphify
$(COMPOSE) --profile cli run --rm \
-v "$(SRC):/src:ro" \
$(if $(ANTHROPIC_API_KEY),-e ANTHROPIC_API_KEY=$(ANTHROPIC_API_KEY)) \
$(if $(OPENAI_API_KEY),-e OPENAI_API_KEY=$(OPENAI_API_KEY)) \
$(if $(GEMINI_API_KEY),-e GEMINI_API_KEY=$(GEMINI_API_KEY)) \
graphify extract /src --code-only --output /data

## Tail MCP server logs
logs:
$(COMPOSE) logs -f mcp
71 changes: 64 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,19 +472,76 @@ The MCP server gives your assistant structured access: `query_graph`, `get_node`
| `--stateless` | off | No per-session state (for load-balanced / CI deployments) |
| `--session-timeout` | `3600` | Reap idle stateful sessions after N seconds (`0` disables) |

The default `127.0.0.1` bind is loopback-only. Set `--host 0.0.0.0` **and** `--api-key` together when exposing on a shared host. Run it in a container:

```bash
docker build -t graphify .
docker run -p 8080:8080 -v "$(pwd)/graphify-out:/data" graphify \
/data/graph.json --transport http --host 0.0.0.0 --api-key "$SECRET"
```
The default `127.0.0.1` bind is loopback-only. Set `--host 0.0.0.0` **and** `--api-key` together when exposing on a shared host.

> **WSL / Linux note:** Ubuntu ships `python3`, not `python`. Use a venv to avoid conflicts:
> ```bash
> python3 -m venv .venv && .venv/bin/pip install "graphifyy[mcp]"
> ```

### Running in a container

A pre-built multi-platform image (`linux/amd64` + `linux/arm64`) is published to GHCR on every release — no local Python install required. Works with Docker and Podman.

**Quick start with `make` (recommended):**

```bash
# Pull from GHCR — no build step needed
make pull

# Index a codebase (mounts current directory read-only)
make index

# Index a different directory
make index SRC=/path/to/project

# Start the MCP HTTP server
GRAPHIFY_API_KEY=secret make up
```

**Or use `docker compose` / `podman compose` directly:**

```bash
# Pull the pre-built image
docker compose pull # or: podman compose pull

# Code-only index (no API key needed)
docker compose --profile cli run --rm graphify extract /src --code-only --output /data

# Full semantic index (pass your LLM key)
docker compose --profile cli run --rm \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
graphify extract /src --backend claude --output /data

# Start the MCP HTTP server
GRAPHIFY_API_KEY=secret docker compose up mcp
```

**Or bare `docker` / `podman` (no compose):**

```bash
# Code-only index
docker run --rm \
-v "$(pwd):/src:ro" \
-v "$(pwd)/graphify-out:/data/graphify-out" \
ghcr.io/graphify-labs/graphify:latest \
extract /src --code-only --output /data

# Serve
docker run -p 8080:8080 \
-v "$(pwd)/graphify-out:/data:ro" \
-e GRAPHIFY_API_KEY="$SECRET" \
--entrypoint graphify-mcp \
ghcr.io/graphify-labs/graphify:latest \
/data/graph.json --transport http --host 0.0.0.0
```

Point your AI assistant at `http://localhost:8080/mcp` with the same key.

> **Podman note:** `make` and `docker-compose.yaml` auto-detect `podman` when `docker` is not in `PATH`. On SELinux systems (RHEL, Fedora, CoreOS) append `:Z` to volume paths if you see permission errors.

> **Override the image:** set `GRAPHIFY_IMAGE=ghcr.io/your-org/graphify:custom` to use a custom registry or tag in compose. Set `RUNTIME=podman` to force Podman in the Makefile.

---

## Environment variables
Expand Down
Loading