Skip to content

Docker Deployment

Quick start

docker run -v scholar-mcp-data:/data/scholar-mcp \
           ghcr.io/pvliesdonk/scholar-mcp:latest

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:

volumes:
  - ./data/scholar-mcp:/data/scholar-mcp

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:

services:
  scholar-mcp:
    build:
      context: .
      args:
        APP_UID: 1000
        APP_GID: 1000

Remote debugging

Production images ship without debugpy to keep the image lean. To attach a remote Python debugger from VS Code or PyCharm:

  1. Build with the debug extra:

    docker build --build-arg DEBUG=true -t scholar-mcp:debug .
    

    This installs the [debug] optional-dependency group (which pulls debugpy transitively from fastmcp-pvl-core). Default builds (DEBUG=false) skip it.

  2. 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:debug
    
    Env var Effect
    SCHOLAR_MCP_DEBUG_PORT TCP port the debugger listens on (any value parsing to 0 disables; non-numeric or out-of-range values log a WARNING and the listener stays off)
    SCHOLAR_MCP_DEBUG_WAIT When truthy (1/true/yes/on), block startup until the IDE attaches. Default is non-blocking.
  3. 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.