built by the makers of Gamut
Remote transport

Deploy an MCP server with Docker.

Updated September 12, 2026

"Deploy an MCP server with Docker" is two jobs. One is packaging a local stdio server so a desktop client can docker run -i. The other is running a Streamable HTTP server as a container behind a URL. Mixing them is the usual failure: a published port on a process that only reads stdin, or a URL client pointed at an image that never opened a socket.

DialMCP is already hosted. You do not containerize it. Connect https://mcp.dialmcp.com/mcp over Streamable HTTP with OAuth 2.1. This page is for operators shipping their own MCP server. Need phone calls from an agent instead? Install DialMCP.

Pick the transport before the Dockerfile

HTTP+SSE from protocol version 2024-11-05 is deprecated. Do not put -t sse in a new image. The category explainer is remote MCP servers; the one-file walk from stdio to a URL is the MCP server tutorial.

Shape 1: a stdio image (local)

The container is the server process. The client must keep stdin open. Docker’s own MCP packaging notes (January 2025) use docker run -i --rm and recommend multi-platform images (linux/amd64 and linux/arm64) because desktop clients run on both.

{
  "mcpServers": {
    "notes": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "--pull=always", "your-account/notes-mcp"]
    }
  }
}

That block belongs in a local config file (Claude Desktop’s claude_desktop_config.json, Cursor’s mcp.json), not in VS Code’s servers key without "type": "stdio". File shapes: MCP server configuration examples.

Shape 2: a Streamable HTTP image (remote)

The container listens. The official Python SDK’s mcp.run(transport="streamable-http") serves a Starlette app with uvicorn. Defaults are 127.0.0.1:8000 and path /mcp. Inside Docker, 127.0.0.1 is unreachable from the host, so bind 0.0.0.0. Source: Python SDK: running your server (checked September 12, 2026).

For anything you actually deploy, hand streamable_http_app() to uvicorn rather than calling mcp.run() as PID 1:

from mcp.server import MCPServer
from mcp.server.transport_security import TransportSecuritySettings

mcp = MCPServer("notes")

@mcp.tool()
def ping() -> str:
    """Tiny tool so you can see the server answer."""
    return "ok"

security = TransportSecuritySettings(
    allowed_hosts=["mcp.example.com", "mcp.example.com:*"],
    allowed_origins=["https://app.example.com"],
)
app = mcp.streamable_http_app(transport_security=security)
FROM python:3.12-slim
WORKDIR /app
COPY server.py .
RUN pip install --no-cache-dir "mcp[cli]" uvicorn
EXPOSE 8000
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]
docker build -t notes-mcp:http .
docker run --rm -p 8000:8000 notes-mcp:http
# Client URL while you are still on the laptop:
# http://127.0.0.1:8000/mcp

The 421 that looks like a dead container

Out of the box the SDK only accepts localhost / 127.0.0.1 Host headers. That is DNS-rebinding protection, and it is correct on your laptop. Behind a real hostname every request comes back 421 Misdirected Request / “Invalid Host header” until you pass transport_security=. The 421 is plain HTTP, not JSON-RPC, so the MCP client reports a generic transport error. Source: Python SDK: deploy and scale (checked September 12, 2026).

Reverse proxy, TLS, Origin

The spec requires Origin validation on Streamable HTTP to stop DNS rebinding. An invalid Origin is HTTP 403. Terminate TLS at the proxy; clients expect https://…/mcp. If the server returns an SSE body for a long tool call, send X-Accel-Buffering: no so nginx does not hold the stream. Keep-alives on long subscriptions/listen streams are SSE comment lines, not a second GET endpoint (GET was removed in 2026-07-28).

Auth

Authorization is optional in the spec and expected the moment the URL is public and the tools touch user data. HTTP MCP servers follow the OAuth 2.1 framework MCP added in 2025-03-26. DialMCP’s hosted path is documented on OAuth and phone verification: browser sign-in, then SMS of a US or Canadian mobile number, no API key. If you are not ready to be an OAuth resource server, do not publish the container.

Workers on 2026-07-28

A modern request is one self-contained POST. There is no Mcp-Session-Id and nothing for a load balancer to stick on. Scale with uvicorn server:app --workers 4 (or your platform’s process manager). Sticky sessions are a legacy-client problem. If you use multi-round-trip tools, share the requestState key across workers; the SDK default is os.urandom(32) per process.

What not to containerize

Go-live checklist

When to stop building

If the job is “give this agent a phone,” do not start a Dockerfile. Connect the hosted server with the snippets on install and client setup. If the job is “ship my own tools,” Docker is the packaging step after the build walkthrough works on localhost. Back to the docs hub.