Blog / Tutorials / How to Host a Remote MCP Server on a VPS: Docker, HTTPS, Auth
(last updated: )

How to Host a Remote MCP Server on a VPS: Docker, HTTPS, Auth

A remote MCP server exposes tools to AI clients over HTTP instead of through a local process. To host one, you package the server in a container, run it on a VPS behind HTTPS and require authentication before any client can call a tool. The…

12 min read

A remote MCP server exposes tools to AI clients over HTTP instead of through a local process. To host one, you package the server in a container, run it on a VPS behind HTTPS and require authentication before any client can call a tool. The current transport is Streamable HTTP, and a small server runs comfortably on a modest VPS.

What Is a Remote MCP Server (and How Is It Different from Local)?

The Model Context Protocol (MCP) is an open protocol, built on JSON-RPC, that lets an AI application discover and call tools exposed by a separate server. Where that server runs separates the two kinds. A local MCP server uses the stdio transport: the client launches it as a subprocess and talks to it over standard input and output, so only that client on that machine can use it. A remote MCP server is an independent process behind a single HTTP endpoint such as https://mcp.example.com/mcp, and any client that can reach the URL and present valid credentials can call it.

The remote transport is called Streamable HTTP. It replaced the older HTTP+SSE transport, which the spec has deprecated since protocol version 2025-03-26 and tells new implementations not to adopt. The current revision, 2026-07-28, simplified it further: every JSON-RPC message is its own POST, and the standalone GET stream and protocol-level sessions are gone. Clients upgrade at different speeds, so use an SDK that serves both eras (the Python SDK below does).

Teams host their own for three reasons: a shared tool set, an always-on endpoint that does not depend on someone’s laptop, and private data that should stay on infrastructure they control.

AspectLocal (stdio)Remote (Streamable HTTP)
Where it runsSubprocess launched by the client on the same machineIndependent process on a server, behind one HTTP endpoint
Who can reach itOnly the client that launched itAny client that can reach the URL and authenticate
AuthenticationNone at the protocol level; credentials come from the environmentAuthorization header on every request; OAuth-based flow in the spec
Typical usePersonal tools that need local files or processesShared team tools, always-on integrations, private data services
Main trade-offSimple, but tied to one machine and one userReachable by everyone, so you own HTTPS, auth, logging and updates

MCP Server Hosting Requirements: What Size VPS Do You Need?

Most MCP servers are thin wrappers around an API, so the MCP layer itself costs little. Your tools decide the size. For MCP server hosting, size by the heaviest tool, start small and watch real usage with docker stats before you upgrade.

Server typeWhat drives resource useStarting point
API wrapperA Python or Node process forwarding calls to other APIs1 vCPU, 1-2 GB RAM
Database-backedQueries plus the database itself, if you run it on the same VPS2 vCPU, 4 GB RAM or more
Browser or code runnerHeadless browser instances or sandboxed code, each with its own memory footprint4 vCPU, 8 GB RAM or more

Several small servers can share one VPS behind a single reverse proxy, which is where a RAM-heavy plan pays off.

Build a Minimal MCP Server You Can Deploy

This example uses the official Python SDK, the mcp package, version 2.3.0 at the time of writing. If you follow older tutorials, note that FastMCP was renamed MCPServer in 2.x and the old import path now fails. The server below has one tool, a health endpoint and the Streamable HTTP transport. It reads its public hostname from an environment variable because the SDK only answers requests addressed to hosts you allow (anything else gets 421 Misdirected Request), which guards against DNS rebinding.

# server.py
import os

from mcp.server import MCPServer
from mcp.server.transport_security import TransportSecuritySettings
from starlette.requests import Request
from starlette.responses import JSONResponse

mcp = MCPServer("word-tools")


@mcp.tool()
def word_count(text: str) -> int:
    """Count the words in a piece of text."""
    return len(text.split())


@mcp.custom_route("/health", methods=["GET"])
async def health(request: Request) -> JSONResponse:
    return JSONResponse({"status": "ok"})


if __name__ == "__main__":
    host = os.environ["MCP_PUBLIC_HOST"]
    mcp.run(
        transport="streamable-http",
        host="0.0.0.0",
        port=8000,
        stateless_http=True,
        transport_security=TransportSecuritySettings(
            allowed_hosts=[host, f"{host}:*"]
        ),
    )

Three details matter. The MCP endpoint defaults to /mcp. The stateless_http option removes session tracking, so you need no sticky sessions if you run more than one copy. And custom routes such as /health are never authenticated by the SDK, so return nothing sensitive from them.

Deploy the MCP Server with Docker on a VPS

Install Docker first; our guide to install Docker on Ubuntu covers it step by step. Then put a Dockerfile and Compose file next to server.py. The container runs as a non-root user, restarts automatically and publishes its port on 127.0.0.1 only, so only a proxy on the same host reaches it.

# Dockerfile
FROM python:3.13-slim
RUN pip install --no-cache-dir "mcp==2.3.0"
RUN useradd --create-home --uid 10001 mcp
USER mcp
WORKDIR /app
COPY --chown=mcp:mcp server.py .
EXPOSE 8000
CMD ["python", "server.py"]
# docker-compose.yml
services:
  mcp:
    build: .
    restart: unless-stopped
    environment:
      MCP_PUBLIC_HOST: mcp.example.com
    ports:
      - "127.0.0.1:8000:8000"
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"]
      interval: 30s
      timeout: 5s
      retries: 3
docker compose up -d --build
docker compose ps
curl -i http://127.0.0.1:8000/health

The last command should return 200 with {“status”:”ok”}. Replace mcp.example.com with a hostname whose DNS record points at your VPS.

Secure Your Remote MCP Server: HTTPS, Authentication and Limits

An open MCP endpoint lets anyone who finds the URL run your tools, so never leave it open. These steps add HTTPS, a bearer-token check and basic limits.

  1. Open only ports 80, 443 and SSH. Our UFW firewall guide shows the commands. Caddy needs 80 and 443 for automatic certificates, and your DNS A or AAAA record must already point at the server.
  2. Create a token with openssl rand -hex 32 and save it in a .env file next to docker-compose.yml as MCP_TOKEN=<value>. Keep that file out of version control.
  3. Add Caddy as a second service using the Caddyfile and service block below. It terminates HTTPS, gets certificates from Let’s Encrypt or ZeroSSL on its own, and rejects requests without the right Authorization header.
  4. Test from outside: a request without the token, or with a wrong one, must return 401. Then add the server to a client with the token set.
  5. Limit the tools. Expose only what you need, keep read-only and write tools on separate servers with separate tokens, and rate limit expensive or destructive tools per token.
# Caddyfile (in ./caddy/)
mcp.example.com {
    @no_token not header Authorization "Bearer {$MCP_TOKEN}"
    respond @no_token "Unauthorized" 401
    reverse_proxy 127.0.0.1:8000
}
# add under services: in docker-compose.yml
  caddy:
    image: caddy:2.11
    restart: unless-stopped
    network_mode: host
    env_file: .env
    volumes:
      - ./caddy:/etc/caddy
      - caddy_data:/data

# add at the top level of docker-compose.yml
volumes:
  caddy_data:

If you already run Nginx Proxy Manager, the same idea works there with a proxy host and a header check. Mount the Caddy configuration as a directory, and persist /data so certificates survive restarts.

This shared bearer token is a gate, not the authorization model in the MCP spec. The spec makes authorization optional, but for HTTP transports it expects an OAuth 2.1-based flow: your server acts only as a resource server, publishes OAuth Protected Resource Metadata (RFC 9728), takes the access token from the Authorization header on every request, never the query string, and rejects tokens not issued for it. The Python SDK supports that with a token verifier plus an authorization server that issues the tokens. A static token has no per-user identity and no expiry, so rotate it by hand and treat every holder as fully trusted. Move to OAuth when people need different permissions or a client only supports OAuth.

Authentication does not stop prompt injection. It controls who may call your tools, not what a model does with the text they return. If a tool fetches a web page, an email or a ticket, instructions hidden in that content can steer an agent that also has write-capable tools. The spec’s answer is layered: servers must validate inputs, enforce access controls, rate limit and sanitize outputs, and clients should keep a human able to deny tool calls. Keep tools least-privilege, avoid pairing private-data tools with tools that read untrusted content or send data out, and log calls. That limits the damage; it does not make a server safe. Our VPS security checklist covers the host side.

Connect Claude, Cursor and Other Clients to Your MCP Server

Each client below can add a remote server by URL. Use the full endpoint and send the token as a header.

  • Claude Code: run claude mcp add –transport http with a name, the URL and an –header option. Its docs call the SSE transport deprecated.
  • Cursor: add a url and headers entry to .cursor/mcp.json (project) or ~/.cursor/mcp.json (all projects). Cursor supports Streamable HTTP and can read the token from an environment variable.
  • VS Code: add a server of type http to .vscode/mcp.json. VS Code tries Streamable HTTP first and falls back to SSE; an inputs prompt keeps the token out of the file.
  • Claude (claude.ai, Desktop and mobile): add a custom connector under Customize > Connectors with the HTTPS URL. It connects from Anthropic’s cloud, so the server must be publicly reachable. The sign-in options are OAuth or none; fixed request headers are in beta for a limited set of organizations.

claude mcp add –transport http word-tools https://mcp.example.com/mcp \
  –header “Authorization: Bearer $MCP_TOKEN”

// .cursor/mcp.json
{
  "mcpServers": {
    "word-tools": {
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer ${env:MCP_TOKEN}" }
    }
  }
}
// .vscode/mcp.json
{
  "inputs": [
    { "type": "promptString", "id": "mcp-token", "description": "MCP token", "password": true }
  ],
  "servers": {
    "word-tools": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer ${input:mcp-token}" }
    }
  }
}

If a connection fails, check these first:

  • 401 on every request: the header is missing, lacks the Bearer prefix, or differs from MCP_TOKEN in .env.
  • 421 Invalid Host header: the client’s hostname is not in allowed_hosts. Fix MCP_PUBLIC_HOST and rebuild.
  • Certificate errors: DNS must point at the VPS, and ports 80 and 443 must be reachable so Caddy can issue the certificate.
  • Hangs or drops: use the /mcp URL, not an /sse one (Claude treats a URL ending in /sse as the older transport), and make sure no proxy buffers streamed responses.

Monitor, Log and Update a Self-Hosted MCP Server

A self-hosted MCP server fails quietly: the agent just stops using the tool.

  • Log: record tool name, timestamp, duration, which token was used and every 401 from the proxy. Never log full arguments or tokens; the spec warns that tokens logged on a server can be stolen. Read container output with docker compose logs -f mcp.
  • Alert: the container healthcheck covers the app; an external monitor on the HTTPS endpoint covers DNS, certificates and the proxy. Our Uptime Kuma guide covers uptime alerts, and the Prometheus and Grafana guide adds metrics.
  • Update: pin versions, rebuild with docker compose build –pull, then run docker compose up -d. Keep the previous image for rollback and read the SDK changelog before any major bump, because 2.x renamed FastMCP.

Why Host MCP Servers on a Contabo VPS

A remote MCP server needs an always-on machine and a predictable bill. On Contabo’s Core line the selling point is RAM per Euro, so a Cloud VPS with 8 GB of RAM or more can host several small MCP servers behind one Caddy instance with room left for a database. The monthly price is fixed, which suits a service that runs around the clock. If data location matters for your private tools, check the available data-centre locations at checkout.

According to Contabo’s firewall launch post, a firewall is included with every VPS and VDS, which adds a second filtering layer in front of the host firewall. For sizing beyond MCP, see our VPS specs for AI agents guide; if your tools call a local model, the GPU VPS launch post covers that hardware. Plan names and prices change, so confirm them on the live Contabo pages before you order.

FAQ: Remote MCP Servers

What is a remote MCP server?

A remote MCP server is a Model Context Protocol server that runs on a network host and serves AI clients over HTTP using the Streamable HTTP transport. Unlike a local stdio server, many clients can reach it by URL, so it needs HTTPS and authentication.

Do I need a VPS to host an MCP server?

No, you can run one on a laptop or a serverless platform. A VPS fits when you want an always-on endpoint, a fixed monthly cost and control over your data. A small API-wrapper server needs very little RAM, so even an entry-level VPS can work.

How do I secure a remote MCP server?

Serve it over HTTPS, require an Authorization header on every request, bind the container to localhost behind a reverse proxy and expose only the tools you need. For multi-user setups use the OAuth-based flow from the spec. None of this removes prompt-injection risk, so keep tools least-privilege.

Can I run an MCP server in Docker?

Yes. An MCP server is an ordinary process, so it runs well in a container. Use a slim base image, a non-root user, a restart policy and a healthcheck, and publish the port on 127.0.0.1 so only your reverse proxy can reach it.

What is the difference between local and remote MCP servers?

A local MCP server runs as a subprocess of one client and talks over stdio, so only that client can use it. A remote MCP server runs independently behind an HTTP endpoint using Streamable HTTP, so many clients can share it, which adds HTTPS, authentication and monitoring.

Disclaimer: Product specifications, features, and prices mentioned in this article are subject to change and may vary by region, billing term, and active promotions. Please check each provider’s or brand’s official website for current figures, pricing, and local currency rates.