The Model Context Protocol (MCP) is what lets Claude, Cursor, and other AI clients reach into your own tools, data, and internal APIs. Hosted MCP services are convenient, but they sit in the middle of every request — seeing your prompts, your credentials, and the results coming back. Running your own MCP server on a VPS removes that middleman entirely. Your queries stay private, your API keys never leave your infrastructure, and you can run any MCP server you want without per-seat pricing or vendor limits.
This guide walks through the full setup end to end: provisioning a VPS, deploying the MCP container with Docker Compose, putting it behind an NGINX reverse proxy with Let’s Encrypt TLS, locking down the firewall and SSH, and pointing your AI client at the new endpoint. By the time you reach Step 9, you’ll have a production-grade MCP server reachable at your own domain, running on hardware you control, with backups and a clear path to scale.
Step 1: Confirm Your Prerequisites
Before you touch a terminal, ensure you have the essential accounts and tools this setup depends on. This prevents the most common self-hosting pitfall: halting deployment midway due to missing DNS, image pulls, or container tooling. For a single MCP server handling light to moderate AI workflow traffic, 1 vCPU and 2 GB RAM is a practical baseline. That’s enough for Docker, a reverse proxy, and the MCP container without the box constantly swapping to disk — a scenario where a “cheap VPS” feels expensive.
| Requirement | Minimum | Why you need it | What to check now |
|---|---|---|---|
| VPS account | Active account | You need a Linux server with root or sudo access | Can you deploy an Ubuntu 22.04 or Debian 12 VPS? |
| Domain registrar | One domain/subdomain | Needed for a stable hostname and HTTPS later | Do you control DNS for mcp.yourdomain.com? |
| Container registry account | Docker Hub account | Useful for pulling public images and avoiding pull limits | Can you log in at hub.docker.com? |
| VPS size | 1 vCPU, 2 GB RAM | Baseline for one MCP container plus proxy overhead | Confirm the instance has at least 20 GB SSD |
| Docker Engine | 20.10+ | Required to run the MCP container reliably | docker --version |
| Docker Compose | 1.29+ or Compose plugin v2 | Needed if the deployment uses compose.yml | docker-compose --version or docker compose version |
| SSH client | Any modern client | You’ll manage the server over SSH | Test login from your laptop |
The key takeaway: if you have these seven items ready, the rest of the setup is straightforward terminal work rather than account-recovery busywork. If anything in the table is missing, fix that first. With everything prepared, the next step is choosing and provisioning the VPS.
Step 2: Provision Your VPS Instance

For MCP, region choice matters more than raw CPU. If your app backend or AI client mostly accesses APIs from New York, Ashburn, or Toronto, place your VPS on the same side of the ocean. Aim for under 50 ms round-trip latency to keep request/response overhead minimal. As a general guide, same-city or same-region traffic often falls in the 5–20 ms range, same-continent around 20–50 ms, and cross-ocean connections might hit 80–150+ ms.
Use your provider’s panel to create a KVM VPS with your Linux image from Step 1. In the region picker, match the server’s location to where your MCP clients run, not necessarily where you live. For example, if your automation workers run in Frankfurt and your laptop is in Chicago, Frankfurt is typically the right choice. If your users are in Southeast Asia, Singapore will generally outperform any US region by reducing triple-digit millisecond delays.
Most panels ask for five key details: region, OS image, plan size, hostname, and SSH access. Choose Ubuntu 22.04 LTS or Debian 12, stick with the baseline size from Step 1, and add your SSH public key during provisioning. This provides a faster, safer passwordless login rather than enabling root password authentication.
The expected output should show your instance as running with a public IPv4 address. After boot, note the default SSH user. On Ubuntu it is commonly ubuntu; on Debian, debian or root may be used depending on the image. If the image disables direct root login, that’s normal — use the default user and escalate privileges with:
sudo -i
Your shell prompt should change to root@your-hostname, confirming that root privileges are available for subsequent installation steps.
Step 3: Install Docker and Dependencies
Once your VPS is live, the next step is to install the runtime that will host the MCP service. Containers resolve the “it works on my machine” problem by packaging the app with its dependencies, ensuring that your server runs the same image tested by the author. This section covers updating the OS, installing Docker Engine, adding the Compose plugin, and enabling Docker to start on boot.
sudo apt update && sudo apt upgrade -y sudo apt install -y ca-certificates curl gnupg lsb-release sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo systemctl enable --now docker
Verify Docker and Compose installation with:
docker --version docker compose version sudo systemctl status docker --no-pager
If you prefer a Docker VPS hosting setup for efficient container management, this is the ideal method. For RHEL-compatible systems, use the native package manager outlined in the article.
To avoid prepending sudo for every Docker command, add your SSH user to the docker group:
sudo usermod -aG docker "$USER" newgrp docker docker ps
Verifying that docker ps works without sudo confirms that the environment is correctly set up for the MCP container.
Step 4: Deploy the MCP Server Container

With Docker operational, the next task is to ensure that your MCP server container starts reliably, restarts on reboots, and handles load predictably. Using Docker Compose is preferred over ad-hoc commands; it consolidates your configuration into one file that can be versioned and redeployed easily.
Create a new directory and add a docker-compose.yml file as follows:
services:
mcp-server:
image: ghcr.io/example/mcp-server:1.2.0
container_name: mcp-server
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
API_KEY: "replace-with-a-long-random-key"
PORT: "3000"
ALLOWED_ORIGINS: "https://app.yourdomain.com,https://admin.yourdomain.com"
mem_limit: 768m
cpus: 0.75This configuration prevents the common “works once, breaks on reboot” issue while setting resource limits. The API_KEY is the shared secret used for client authentication, PORT indicates the internal bind port, and ALLOWED_ORIGINS sets a comma-separated allowlist to restrict browser-based requests. The bind of 127.0.0.1:3000:3000 keeps the app private to the VPS, pending the reverse proxy configuration.
The specified memory and CPU limits ensure that on a 2 GB VPS, the container reserves enough headroom for the OS, Docker, and any reverse proxy you introduce later. To run the container in the background, execute:
mkdir -p ~/mcp-server && cd ~/mcp-server nano docker-compose.yml docker compose up -d
Verify the deployment with:
docker compose ps docker logs --tail=50 mcp-server
For a comparison between single container and Compose deployments, keep in mind that Compose supports repeatability and versioning — a considerable advantage over one-off docker run commands. Also, pin your image to a specific version, such as :1.2.0, to prevent issues caused by silent upstream changes.
Step 5: Configure a Reverse Proxy and SSL
Since the container listens only on 127.0.0.1:3000, it is inaccessible externally. The solution is to set up an NGINX reverse proxy that accepts public traffic, forwards MCP-specific requests to the container, and terminates TLS. First, update your DNS A record for mcp.yourdomain.com to point to your VPS’s IP address, then install NGINX and Certbot:
sudo apt update sudo apt install -y nginx certbot python3-certbot-nginx
Verify installation with:
nginx -v certbot --version
The following NGINX server block binds your custom domain, proxies MCP requests, and redirects HTTP traffic to HTTPS:
server {
listen 80;
listen [::]:80;
server_name mcp.yourdomain.com;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name mcp.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/mcp.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp.yourdomain.com/privkey.pem;
location /mcp/ {
proxy_pass http://127.0.0.1:3000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}Save the configuration as /etc/nginx/sites-available/mcp and enable it:
sudo nano /etc/nginx/sites-available/mcp sudo ln -s /etc/nginx/sites-available/mcp /etc/nginx/sites-enabled/mcp sudo nginx -t && sudo systemctl reload nginx
Verify the reverse proxy:
curl -I http://mcp.yourdomain.com/mcp/
After confirming a 301 Moved Permanently response, issue the TLS certificate using Certbot:
sudo certbot --nginx -d mcp.yourdomain.com
This resolves self-signed certificate warnings and sets up auto-renewal. Verify HTTPS end-to-end with:
curl -I https://mcp.yourdomain.com/mcp/ systemctl list-timers | grep certbot
For more insights into setting up self-hosted services, consider reading our article on Mastering Self-Hosting: Take Control of Your Apps and Data.
Step 6: Harden Your Server Security

Your MCP endpoint is now accessible over HTTPS. To safeguard it, limit open ports to only the essentials — typically three: SSH for admin access, HTTP for certificate renewals, and HTTPS for client traffic. Implement a default-deny firewall with UFW via:
sudo apt install -y ufw sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow 22/tcp comment 'SSH admin access' sudo ufw allow 80/tcp comment 'HTTP for redirect and ACME challenge' sudo ufw allow 443/tcp comment 'HTTPS for MCP endpoint' sudo ufw enable sudo ufw status numbered
Confirm that only ports 22, 80, and 443 are open with:
sudo ufw status verbose ss -tulpn
Next, use fail2ban to block SSH brute-force attacks:
sudo apt install -y fail2ban sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF' [sshd] enabled = true port = 22 logpath = %(sshd_log)s maxretry = 5 findtime = 10m bantime = 1h EOF sudo systemctl enable --now fail2ban
Verify with:
sudo fail2ban-client status sshd
Finally, disable direct root SSH login and require key-based authentication by editing /etc/ssh/sshd_config:
# /etc/ssh/sshd_config PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes
Apply and verify the SSH configuration changes:
sudo sshd -t && sudo systemctl reload ssh grep -E 'PermitRootLogin|PasswordAuthentication|PubkeyAuthentication' /etc/ssh/sshd_config
Step 7: Connect Your MCP Client
With the server secured and reachable via HTTPS, point your MCP client at it so your automations transition from localhost to the newly built VPS endpoint. For Claude Desktop, update the MCP client JSON to reflect your domain and API key. For example, if the reverse proxy exposes the service at https://mcp.yourdomain.com/mcp/, the configuration might be:
{
"mcpServers": {
"private-vps-mcp": {
"transport": {
"type": "http",
"url": "https://mcp.yourdomain.com/mcp/"
},
"headers": {
"Authorization": "Bearer REPLACE_WITH_YOUR_LONG_API_KEY"
}
}
}
}After saving the configuration in your client’s MCP settings, test connectivity with:
curl -i https://mcp.yourdomain.com/mcp/ \ -H "Authorization: Bearer REPLACE_WITH_YOUR_LONG_API_KEY" \ -H "Content-Type: application/json"
A successful response (e.g., 200 OK or 202 Accepted) confirms proper connectivity to your MCP server.
Step 8: Verify Operation and Monitor

Before going live, verify that your endpoint responds over HTTPS, the container remains healthy, and you’ll be promptly alerted if it goes down. Perform a 30-second check to avoid discovering issues only after an automation fails.
curl -i https://mcp.yourdomain.com/mcp/health # or http https://mcp.yourdomain.com/mcp/health docker logs --tail=100 mcp-server docker ps --filter "name=mcp-server" docker restart mcp-server
For external monitoring, tools like UptimeRobot can check your health URL every minute or every five minutes for non-200 responses. This not only confirms that your MCP container is running but also that it’s ready to support your operations. Additionally, our guide on Self Hosted Backup Software: Tools, Setup, and Best Practices provides further tips on maintaining operational resiliency.
Step 9: Next Steps and Cleanup
Once your health checks remain stable, focus on two aspects that make self-hosting sustainable: recoverability and easy scaling. Create a backup of your current Docker configuration and application data before making major updates:
mkdir -p ~/backups/mcp-$(date +%F) cp docker-compose.yml ~/backups/mcp-$(date +%F)/ docker inspect mcp-server > ~/backups/mcp-$(date +%F)/container-inspect.json tar -czf ~/backups/mcp-$(date +%F)/mcp-data.tar.gz /opt/mcp-data 2>/dev/null || true
Verify your backup with:
ls -lah ~/backups/mcp-$(date +%F)
For scaling, if one container becomes a bottleneck, horizontally scale behind NGINX by running:
docker compose up -d --scale mcp-server=3 docker ps --filter "name=mcp-server"
When necessary, safely tear down the setup:
docker compose down docker image prune -f docker volume prune -f
Verify cleanup with:
docker ps -a docker system df
This comprehensive guide ensures you can set up, secure, monitor, and scale your MCP server confidently on a VPS using self-hosting best practices.
Frequently Asked Questions
What are the minimum server requirements for MCP setup on a VPS?
How do I secure my self-hosting environment after deployment?
Why should I use Docker Compose instead of a single container deployment?
How can I verify that my MCP container is running properly?
What are the benefits of deploying MCP on a VPS using a reverse proxy?