Docker Deployment¶
Quick start¶
The server listens on port 8000 with HTTP transport by default. Add -e SCHOLAR_MCP_S2_API_KEY=your-key for higher rate limits (see below).
Docker Compose¶
Basic setup¶
services:
scholar-mcp:
image: ghcr.io/pvliesdonk/scholar-mcp:latest
restart: unless-stopped
environment:
SCHOLAR_MCP_S2_API_KEY: "${SCHOLAR_MCP_S2_API_KEY}"
SCHOLAR_MCP_CACHE_DIR: "/data/scholar-mcp"
volumes:
- scholar-mcp-data:/data/scholar-mcp
volumes:
scholar-mcp-data:
With docling-serve (PDF conversion)¶
services:
scholar-mcp:
image: ghcr.io/pvliesdonk/scholar-mcp:latest
restart: unless-stopped
environment:
SCHOLAR_MCP_S2_API_KEY: "${SCHOLAR_MCP_S2_API_KEY}"
SCHOLAR_MCP_DOCLING_URL: "http://docling-serve:5001"
SCHOLAR_MCP_READ_ONLY: "false"
SCHOLAR_MCP_CACHE_DIR: "/data/scholar-mcp"
SCHOLAR_MCP_CONTACT_EMAIL: "${SCHOLAR_MCP_CONTACT_EMAIL:-}"
volumes:
- scholar-mcp-data:/data/scholar-mcp
docling-serve:
image: ghcr.io/ds4sd/docling-serve:latest
restart: unless-stopped
volumes:
scholar-mcp-data:
With Traefik reverse proxy¶
services:
scholar-mcp:
image: ghcr.io/pvliesdonk/scholar-mcp:latest
restart: unless-stopped
env_file: .env
volumes:
- scholar-mcp-data:/data/scholar-mcp
labels:
- "traefik.enable=true"
- "traefik.http.routers.scholar-mcp.rule=Host(`scholar-mcp.yourdomain.com`)"
- "traefik.http.routers.scholar-mcp.tls.certresolver=letsencrypt"
- "traefik.http.services.scholar-mcp.loadbalancer.server.port=8000"
networks:
- traefik
docling-serve:
image: ghcr.io/ds4sd/docling-serve:latest
restart: unless-stopped
networks:
- traefik
volumes:
scholar-mcp-data:
networks:
traefik:
external: true
Image tags¶
| Tag | Contents | Updated by |
|---|---|---|
latest |
Newest stable release | Each stable release that is newest across all series |
vX.Y.Z |
That exact release (pre-releases included, as vX.Y.Z-rc.N) |
Never (immutable) |
vX.Y, vX |
Newest stable release in that series | Each stable release that is newest in its series |
rc |
Newest release candidate | Each pre-release still ahead of latest |
edge |
Newest commit on main |
Every merge to main |
Rolling tags are ordering-aware: a patch release cut from an old release/X.Y branch after a newer stable has shipped updates its own series tags but never latest. The same rule governs rc: a candidate only moves the tag while its version is still ahead of the newest stable, so a candidate for an already-released version never pulls rc behind latest.
The three rolling tags answer different questions. Use latest to run released code, rc to test the candidate for the next release, and edge to run the newest merged commit. Note that rc is not cleared when its release ships: it keeps pointing at the last candidate until the next one is cut, so latest is the tag to follow in production. To find the commit behind an edge image, read its org.opencontainers.image.revision label:
docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' \
ghcr.io/pvliesdonk/scholar-mcp:edge
Environment variables¶
See Configuration for the full reference. Key variables for Docker:
| Variable | Default | Description |
|---|---|---|
SCHOLAR_MCP_S2_API_KEY |
n/a | Semantic Scholar API key (optional; ~1 req/s without, ~10 req/s with) |
SCHOLAR_MCP_CACHE_DIR |
/data/scholar-mcp |
Cache and PDF storage directory |
SCHOLAR_MCP_READ_ONLY |
true |
Set false to enable PDF tools |
SCHOLAR_MCP_DOCLING_URL |
n/a | docling-serve URL (such as http://docling-serve:5001) |
SCHOLAR_MCP_BEARER_TOKEN |
n/a | Bearer token for HTTP auth |
FASTMCP_LOG_LEVEL |
INFO |
Logging level (use -v or set to DEBUG for verbose output) |
FASTMCP_ENABLE_RICH_LOGGING |
true |
Set false for structured JSON logging with aggregators |
SCHOLAR_MCP_INSTRUCTIONS |
(computed at startup) | System instructions for LLM context |
SCHOLAR_MCP_DEBUG_PORT |
n/a | Remote-debugger TCP port (see Remote debugging; requires --build-arg DEBUG=true image) |
SCHOLAR_MCP_DEBUG_WAIT |
false |
Block startup until IDE attaches (see Remote debugging) |
For OIDC authentication, see OIDC deployment.
Running behind a reverse proxy on a path prefix (https://mcp.example.com/myservice/mcp) rather than its own hostname needs two routing rules, one of which sits outside the prefix: see Subpath Deployments.
Volumes¶
| Container path | Purpose |
|---|---|
/data/scholar-mcp |
SQLite cache database, downloaded PDFs, converted Markdown |
/data/state |
FastMCP OIDC state (only needed with OIDC auth) |
Use named volumes (shown above) for persistence. Bind mounts also work:
UID/GID¶
The image runs as a non-root user with UID/GID 1000 by default. To match your host user for bind mounts, set build args:
Remote debugging¶
Production images ship without debugpy to keep the image lean. To attach a remote Python debugger from VS Code or PyCharm:
-
Build with the debug extra:
This installs the
[debug]optional-dependency group (which pullsdebugpytransitively fromfastmcp-pvl-core). Default builds (DEBUG=false) skip it. -
Run with the debug env vars set and the port mapped:
docker run --rm \ -e SCHOLAR_MCP_DEBUG_PORT=5678 \ -e SCHOLAR_MCP_DEBUG_WAIT=true \ -p 127.0.0.1:5678:5678 \ -p 8000:8000 \ scholar-mcp:debugEnv var Effect SCHOLAR_MCP_DEBUG_PORTTCP port the debugger listens on (any value parsing to 0disables; non-numeric or out-of-range values log a WARNING and the listener stays off)SCHOLAR_MCP_DEBUG_WAITWhen truthy ( 1/true/yes/on), block startup until the IDE attaches. Default is non-blocking. -
Attach from VS Code, adding a launch config:
{ "name": "Attach to scholar-mcp", "type": "debugpy", "request": "attach", "connect": { "host": "localhost", "port": 5678 } }PyCharm uses Run → Edit Configurations → Python Debug Server with the same host/port.
Never publish the debug port on a public network
The debug listener binds 0.0.0.0 inside the container so the IDE can reach it from the host, but debugpy's DAP protocol is unauthenticated: any peer that can reach the port has arbitrary code execution as the server process. Always bind the port mapping to localhost (-p 127.0.0.1:5678:5678) or tunnel via kubectl port-forward / SSH. Production images should be built with default DEBUG=false.
When the helper is invoked but debugpy isn't installed (say, someone sets DEBUG_PORT on a non-debug image), it logs a WARNING and continues; this is the safe failure mode.