← All articles

Run MCP Server in Docker

July 26, 2025 · 15 min read

Docker is deployment, not transport. Containers package the same MCP server you run on the host; the client still speaks stdio over the container’s stdin and stdout. This guide takes the Echo stdio server from Building MCP Server Using .NET, adds a production-ready Dockerfile, and shows how to build, run, and test it with Claude Desktop.

For remote HTTP-based MCP (Streamable HTTP or legacy SSE), containerize a web API entrypoint and expose a port—see Build MCP Server with Streamable HTTP (recommended) or Build MCP Server with SSE Transport (legacy).

Prerequisites

Project recap

You should already have a working stdio server from the .NET tutorial. The pieces that matter for Docker:

Console app project

Stdio MCP servers run as a console app, not a web API. Create the project with:

dotnet new console -n MyMcpServer
cd MyMcpServer

If you started from dotnet new webapi, switch to a console template or a new console project—stdio transport does not need Kestrel or HTTP listeners.

Required NuGet packages

dotnet add package Microsoft.Extensions.Hosting
dotnet add package ModelContextProtocol

Restore locally to confirm the project compiles: dotnet restore && dotnet build.

  • Transport: WithStdioTransport() — no HTTP port, no MapMcp() required for stdio-only
  • Tools: MyTools with an Echo tool the client can call
  • Logging: write diagnostics to stderr only. stdout carries the JSON-RPC protocol stream; anything else on stdout (including Console.WriteLine) breaks MCP inside and outside Docker

In Program.cs:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateEmptyApplicationBuilder(settings: null);

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

var app = builder.Build();

await app.RunAsync();

From the project root (MyMcpServer/), confirm the server runs locally before containerizing:

dotnet run

Stop it with Ctrl+C once verified. The MCP client (or Claude) will launch the process and attach to stdio—you do not browse to a URL.

Write the Dockerfile

Place a Dockerfile in the project root (same folder as MyMcpServer.csproj). This multi-stage build caches NuGet restores and produces a small runtime image. There is no EXPOSE — stdio servers do not listen on a port.

# Use the official .NET Core SDK image as the base image
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build

# Copy project files
COPY . ./app

# Publish app to out folder
WORKDIR /app
RUN dotnet publish -c Release -o out

# Build the runtime image
FROM mcr.microsoft.com/dotnet/runtime:10.0-alpine AS runtime
WORKDIR /app
COPY --from=build /app/out .

# Entry point when the container starts
ENTRYPOINT ["dotnet", "stdio-mcp-dotnet-docker.dll"]

If your project file has a different name, replace MyMcpServer.csproj and MyMcpServer.dll accordingly.

Build the image

From the directory that contains the Dockerfile:

docker build -t my-mcp-server:latest .

Run and test (without Claude)

Stdio MCP requires an interactive container so the client can write JSON-RPC to stdin and read responses from stdout.

docker run -i --rm my-mcp-server:latest

Manual JSON-RPC test: with the container running, paste this line into the terminal and press Enter:

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}

You should see a JSON response on stdout listing available tools (for example Echo). If nothing appears, the server may require an initialize request first—see the MCP specification. Use stderr for any debug output so stdout stays valid JSON-RPC.

Flag Role
-i Keep stdin open — required for JSON-RPC
--rm Remove the container when the client exits

Smoke test: the container should start and wait (no immediate exit, no crash loop). That means the process is listening on stdio for MCP messages.

Common failures:

  • Missing -i — client cannot send requests; connection fails silently or immediately
  • Wrong image tagdocker images and align the tag with your config
  • Logs on stdout — use stderr for logging; polluted stdout corrupts the protocol
  • Image not rebuilt — old tool definitions after code changes

Test with Claude Desktop

Claude Desktop spawns the MCP server as a subprocess. For Docker, the subprocess is docker run with stdio attached to the container.

Prerequisites

  • Claude Desktop installed
  • Docker running
  • Image built: my-mcp-server:latest

Configure Claude Desktop

Edit claude_desktop_config.json:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "my-mcp-server": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "my-mcp-server:latest"]
    }
  }
}

The -i flag in args is mandatory — without it, JSON-RPC cannot reach the server.

Step-by-step

  1. Build the image: docker build -t my-mcp-server:latest .
  2. Add the config above to claude_desktop_config.json
  3. Fully quit Claude Desktop and relaunch (config is read at startup)
  4. Open Claude and check the MCP/tools indicator (hammer icon) — my-mcp-server should appear as connected
  5. Send a test prompt, for example: "Use the echo tool to repeat the phrase hello docker"
  6. Confirm Claude calls Echo and returns the echoed text

Verify and troubleshoot

  • Claude logs: Help → View Logs (or Developer settings) — look for Docker or MCP connection errors
  • Docker not running: start Docker Desktop before launching Claude
  • Image missing: run docker images and confirm my-mcp-server is listed
  • Stale config: quit Claude completely (not just close the window), then relaunch
  • Forgot -i: connection fails; ensure "args" includes -i before the image name
  • Old image: rebuild after code changes, then restart Claude
  • Protocol errors: check that the app logs only to stderr, not stdout

Related reading

Related articles