← mailrelay-mcp-server

Deploying to a VPS (Hetzner Cloud + Caddy)

This guide puts mailrelay-mcp-server on a small VPS behind a real domain, so MCP clients can connect to a stable HTTPS URL without your workstation being involved.

Architecture:

MCP client  ──HTTPS──▶  Caddy (:443, auto TLS)  ──HTTP──▶  Node (127.0.0.1:3939)
                         on the VPS                         systemd service

The Node process never listens on a public interface — only Caddy does.


0. What this server does and doesn't hold

In HTTP mode the server is multi-tenant and holds no credentials of its own. Each user connects with their own Mailrelay account and API key, entered on the consent screen during the OAuth handshake and verified against Mailrelay before access is granted. There is no shared master token to deploy, rotate, or leak.

Those per-user credentials are stored encrypted (AES-256-GCM) under the service state directory, so restarts and deployments do not disconnect anyone. The encryption key lives in the same config file, which means this protects against disks, backups and snapshots leaving the machine — not against a root compromise of the host itself.

If any Mailrelay API key of yours has previously been pasted into a chat, a screenshot, or a shared terminal, issue a fresh one from Mailrelay → Settings → API and revoke the old one before using it here.

1. Create the server

In the Hetzner Cloud console:

2. Point your domain at it

At your registrar (e.g. EuroDNS), for the subdomain you want to use:

Type Name Value
A mcp the server's IPv4 address
AAAA mcp the server's IPv6 address

Wait until dig +short mcp.yourdomain.com returns the right IP before step 6 — Caddy's certificate request will fail if DNS hasn't propagated.

3. Base system setup

ssh root@YOUR_SERVER_IP
apt update && apt upgrade -y

Install Node.js 24 (the Active LTS line):

curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && apt install -y nodejs git

Both third-party repos used here are distribution-agnostic — NodeSource publishes under a nodistro suite and Caddy's under any-version — so neither depends on the Ubuntu release codename.

Create an unprivileged user to run the service:

adduser --system --group --home /opt/mailrelay-mcp-server mcp

Harden SSH (recommended)

A public IP is scanned within hours; expect thousands of failed logins a day. Make sure none of them can succeed, and cut the noise:

4. Install the application

git clone https://github.com/YOUR_GITHUB_USERNAME/mailrelay-mcp-server.git /opt/mailrelay-mcp-server
cd /opt/mailrelay-mcp-server && npm ci --omit=dev && chown -R mcp:mcp /opt/mailrelay-mcp-server

5. Configure the service

Configuration goes in a root-owned file, not in a .env inside the app directory. systemd reads it as root before dropping privileges, so the app user never needs access to the file itself.

install -m 600 -o root -g root /dev/null /etc/mailrelay-mcp.env

Then edit /etc/mailrelay-mcp.env:

PUBLIC_URL=https://mcp.yourdomain.com
PORT=3939
SESSION_ENCRYPTION_KEY=<output of: openssl rand -hex 32>

No Mailrelay credentials belong here — in HTTP mode each user supplies their own during the OAuth handshake. SESSION_ENCRYPTION_KEY encrypts the stored sessions; generate it on the server so it never passes through a chat or a clipboard. Losing it is survivable: the server starts with no sessions and users reconnect.

PUBLIC_URL must exactly match the URL clients connect to: it is the OAuth issuer identifier, and a mismatch breaks the handshake.

6. Install Caddy and the service

apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list && apt update && apt install -y caddy

Copy the provided config files, editing the domain in the Caddyfile first:

cp /opt/mailrelay-mcp-server/deploy/Caddyfile /etc/caddy/Caddyfile && nano /etc/caddy/Caddyfile
cp /opt/mailrelay-mcp-server/deploy/mailrelay-mcp.service /etc/systemd/system/

Start everything:

systemctl daemon-reload && systemctl enable --now mailrelay-mcp && systemctl reload caddy

7. Verify

Service is up and listening only on loopback:

systemctl status mailrelay-mcp --no-pager && ss -tlnp | grep 3939

TLS works and OAuth discovery is reachable:

curl -s https://mcp.yourdomain.com/.well-known/oauth-authorization-server | head -c 200

The MCP endpoint refuses unauthenticated calls (this should print 401):

curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.yourdomain.com/mcp

Live logs:

journalctl -u mailrelay-mcp -f

8. Connect a client

Any Mailrelay user connects the same way, with their own account:

Claude Desktop → Settings → Connectors → Add custom connector → https://mcp.yourdomain.com/mcp → Connect. A consent screen asks for their Mailrelay account URL and API key; those are checked against Mailrelay, and on success the connector is authorized for that account only.

Claude Code:

claude mcp add --transport http mailrelay https://mcp.yourdomain.com/mcp

The client runs the same OAuth handshake and opens the consent screen in a browser. There is no static token to paste into a config file — access tokens are issued per user by the handshake.

9. Publishing the docs as a website (optional)

Caddy can serve the project docs as static pages on the same domain, with everything else still proxied to the MCP server. Generate them from the Markdown sources:

npm run build:site

That writes three pages into site/: index.html (from README.md), deploy.html (this file) and license.html. Upload them:

scp site/*.html root@YOUR_SERVER_IP:/var/www/mailrelay-mcp/

(Add -P <port> if you moved SSH off port 22.)

The deploy/Caddyfile already contains the routing: a @docs matcher serves those three paths from disk, the catch-all handle proxies to the MCP server, and /DEPLOY.md and /LICENSE redirect to the rendered pages so repo-relative links don't 404. Doc pages stay indexable; every API path gets X-Robots-Tag: noindex, nofollow.

The sources keep repo-relative links (DEPLOY.md, LICENSE) so they resolve correctly on GitHub; the build rewrites them to the published page URLs.

Re-run the build and upload whenever the Markdown changes — site/ is a build artifact and is gitignored, not hand-maintained.

10. Updating

cd /opt/mailrelay-mcp-server && git pull && npm ci --omit=dev && systemctl restart mailrelay-mcp

A restart is required for code changes: Node loads the modules once at startup, so editing files on disk has no effect until the process restarts.


Operational notes