Docs / Self-hosting

Self-hosting

What a self-hosted mcpgate runs, the hardware it needs, and what you must keep. The install itself is on the Quick Start page.

What runs

The install starts two containers:

  • mcpgate: the gateway, image mcpgate/mcpgate. It listens on port 3001 in the container, and on port 8642 on the host.
  • redis: sessions and OAuth tokens, limited to 512 MB.

There is no database server. The audit log and the counters are SQLite files in the volume of the gateway.

Hardware

  • Recommended: 2 vCPU, 4 GB RAM, 40 GB disk. Our own production runs on a cloud vServer of this size, with two gateway containers for blue/green deployment. One gateway needs less.
  • Lower limit, estimated: 2 vCPU and 2 GB RAM for one gateway. We did not test this size.
  • We have no measured sizes per number of users yet.
  • With the optional NER container, see the three setups.

The load on our production server, measured over 7 days up to 30 October 2026 (gateway v2.0.3543). The server ran two gateway containers for blue/green deployment, Redis and nginx:

  • About 29,000 MCP requests. The highest 5-minute average was about 1.1 requests per second.
  • CPU: 10 % of the 2 vCPU on average.
  • RAM of the whole server, the operating system included: 2.2 GB on average, 2.9 GB at most. No out-of-memory kill in 30 days.
  • Disk: 16 GB used, old images included.

Image and host

  • A Linux host with Docker and Docker Compose v2.
  • The image is built for amd64 and arm64. The download is about 435 MB (v2.0.3713, with the spaCy models).

Volumes you must keep

  • gateway-data (/app/data): the audit log, which keeps 90 days by default, the counters and the admin settings. If you lose this volume, you lose the audit trail.
  • redis-data (/data): the sessions and the OAuth tokens. If you lose this volume, every user must log in again.

Back up both volumes.

Secrets

The gateway reads its secrets from the environment. If ENCRYPTION_KEY, JWT_SECRET or SECRET_REVEAL_TOKEN is not set at the first start, the gateway makes one and writes it to gateway.env in gateway-data. The key is then in the same volume as the data that it protects, and in every backup of that volume.

If a secret is set in the environment, the gateway does not write it to the file. To keep the keys away from the data, set them in the environment from a secret store.

  • ENCRYPTION_KEY encrypts the OAuth tokens and the sessions in Redis, and the identities in the audit log. Without this key, a backup of the volumes cannot be read.
  • AUDIT_CHAIN_KEY signs the hash chain of the audit log. If it is not set, the gateway uses ENCRYPTION_KEY.
  • REDIS_PASSWORD is the password of Redis.

A backup is complete only with the two volumes and the secrets.

To keep the key in the volume is a choice that you can make. The admin overview shows where the key is, in the row Encryption at rest of the card Instance readiness: grey for a key in the volume, amber only when something is broken. To keep the key apart from the data, move it as below.

Move the keys out of the volume

Move the existing keys. Do not make new ones: with a new ENCRYPTION_KEY, the gateway cannot read the stored tokens, and every user must connect the services again.

In ~/.mcpgate, run these commands in this order. No key appears on the screen.

  1. Copy the three lines from gateway.env into the .env next to the Compose file:
    cd ~/.mcpgate
    touch .env && chmod 600 .env && printf '\n' >> .env
    docker compose exec -T mcpgate grep -E '^(ENCRYPTION_KEY|JWT_SECRET|SECRET_REVEAL_TOKEN)=' /app/data/gateway.env >> .env
  2. Create the container again, so that it reads .env. Use up -d: docker compose restart does not read .env.
    docker compose up -d
  3. Check that the container has the key. The command prints key in the environment. If it does not, stop here and do not delete anything.
    docker compose exec -T mcpgate sh -c 'test -n "$ENCRYPTION_KEY" && echo "key in the environment"'
  4. Delete the three lines from the volume:
    docker compose exec -T mcpgate sed -i -E '/^(ENCRYPTION_KEY|JWT_SECRET|SECRET_REVEAL_TOKEN)=/d' /app/data/gateway.env
  • The .env file is on the host, not in a volume. Back it up separately, or keep the keys in a secret store and give them to the container from there.
  • Backups of the volume that you made before this step still hold the keys. Protect them, or delete them.
  • An AI client connected to the gateway cannot do this step for you: the gateway cannot change files on the host. An agent that runs in a shell on the gateway host, such as Claude Code, can run the commands above, and the key does not appear in its output.

On AWS

How to run mcpgate on ECS with Fargate. We have not run it on AWS ourselves: these steps follow from the code of the gateway.

Set it up

  1. Store the secrets in AWS Secrets Manager or SSM Parameter Store. For an install that already runs, use the existing keys (see Move the keys out of the volume); new keys make the stored tokens unreadable.
    VariableValue
    ENCRYPTION_KEY32 random bytes in base64: openssl rand -base64 32
    JWT_SECRETRandom, 64 bytes or more: openssl rand -hex 64
    SESSION_SECRETRandom. Without it, the gateway uses ENCRYPTION_KEY.
    SECRET_REVEAL_TOKENRandom: openssl rand -base64 24
    AUDIT_CHAIN_KEYOptional, random. Without it, the gateway uses ENCRYPTION_KEY.
    REDIS_PASSWORDThe password of your Redis.
  2. Give them to the task in the container definition. The task execution role needs secretsmanager:GetSecretValue on these secrets.
    "secrets": [
      { "name": "ENCRYPTION_KEY",      "valueFrom": "arn:aws:secretsmanager:eu-central-1:111122223333:secret:mcpgate/encryption-key" },
      { "name": "JWT_SECRET",          "valueFrom": "arn:aws:secretsmanager:eu-central-1:111122223333:secret:mcpgate/jwt-secret" },
      { "name": "SESSION_SECRET",      "valueFrom": "arn:aws:secretsmanager:eu-central-1:111122223333:secret:mcpgate/session-secret" },
      { "name": "SECRET_REVEAL_TOKEN", "valueFrom": "arn:aws:secretsmanager:eu-central-1:111122223333:secret:mcpgate/reveal-token" },
      { "name": "REDIS_PASSWORD",      "valueFrom": "arn:aws:secretsmanager:eu-central-1:111122223333:secret:mcpgate/redis-password" }
    ],
    "environment": [
      { "name": "AWS_KMS_KEY_ID", "value": "arn:aws:kms:eu-central-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" },
      { "name": "REDIS_URL",      "value": "redis://my-cache.abc123.euc1.cache.amazonaws.com:6379/0" }
    ]
  3. Optional: let AWS KMS protect the stored records. Create a symmetric KMS key and turn on its automatic key rotation. Give the task role these two rights on the key, and set AWS_KMS_KEY_ID to the key ARN, as in the example above. The gateway gets its AWS credentials from the task role; there is no variable for an AWS access key.
    {
      "Effect": "Allow",
      "Action": ["kms:Encrypt", "kms:Decrypt"],
      "Resource": "arn:aws:kms:eu-central-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
    }
  4. Storage: the audit log and the counters are SQLite files in WAL mode, in /app/data. SQLite does not support WAL on a network file system (opens in a new tab). Use a local disk: ECS on EC2 with an EBS volume, or a VM. Run one task only, also with EBS.

    If you use Fargate with EFS, set the deployment of the service to minimumHealthyPercent: 0 and maximumPercent: 100. Then ECS stops the old task before it starts the new one, and only one task writes at any time. An update then has a short downtime. SQLite does not test this setup.

  5. Redis: with ElastiCache, keep its data across a restart: AOF where your node type allows it, else backups. Without persistence, every user must log in again after a restart.
  6. Load balancer: put an Application Load Balancer in front, with an HTTPS listener and a certificate from ACM.
    • Send the traffic to port 3001 of the task over HTTP. Set the health check path to /health.
    • Let only the security group of the load balancer reach port 3001.
    • Set BASE_URL to the public HTTPS address, for example https://gateway.example.com.
    • Set FORWARDED_ALLOW_IPS to the CIDR of the subnets of the load balancer. The gateway takes the client address and the scheme from the X-Forwarded-* headers only from these addresses. The default * trusts every sender, so a client can set its own address for the rate limit.
    • Keep the idle timeout of the load balancer below 120 seconds, the keep-alive time of the gateway. The default of 60 seconds is correct.

Backup: the data volume (EBS or EFS), the Redis data and the secrets. Without the secrets, the other two cannot be read.

If AWS KMS does not answer, sign-in and token checks answer 503 until it answers again. Records that the gateway wrote before AWS KMS, with the local key, stay readable.

Network and HTTPS

The gateway speaks plain HTTP. The install publishes port 8642 on 127.0.0.1 only, so only the same machine reaches it. Docker does not add TLS.

To reach the gateway from other machines, run a reverse proxy with TLS on the same server. The OAuth callbacks need HTTPS. With Caddy (opens in a new tab), which gets the certificate by itself, the configuration is:

gateway.example.com {
    reverse_proxy 127.0.0.1:8642
}

Then write the HTTPS address into ~/.mcpgate/.env, and run docker compose up -d:

BASE_URL=https://gateway.example.com
  • Do not publish port 8642 on all interfaces. Docker writes its own firewall rules, so a host firewall such as ufw does not block a published port.
  • If your proxy runs on another machine, for example a load balancer, change the port line of the Compose file, and allow only the proxy to reach the port.
  • Outbound, the gateway connects to the APIs of the services you enable, to your login provider and to the AI providers you configure.

Name detection (NER)

The rules find values with a fixed form, such as an IBAN or a mail address. A name in free text has no fixed form. Name detection finds it, in two places:

WhereWhat it findsWhat happens
Document reviewPersons, companies, placesSuggestions. A person confirms each one.
Tool responses, with the option Baseline + name detectionPersonsA name becomes a token before the AI sees it.

In tool responses, companies and places stay text. That is usually what you want: they are rarely personal data, and the AI needs them to understand the response.

What you need. The gateway image includes spaCy for German and English. It is enough for the suggestions of document review: no extra container, no setting, about 260 to 320 MB of RAM per language. For better person names, and for tool responses, run the optional NER container mcpgate/ner:v2.0.3584 (amd64 and arm64, about 690 MB).

Two options, in the card Detector: Baseline is detection by the maisecrets library alone. Baseline + name detection also sends the text to the NER container and replaces the names it finds. With Baseline, you can set name detection to Measure: it only counts the names and changes no response.

Before you choose Baseline + name detection:

  • If the container does not answer within 1.5 s, gives an error or is busy, only the rules apply. The privacy notice in the response then says that names in free text pass unchanged.
  • A client that compares names with a list of its own sees tokens, not names. For such a workflow, keep Measure.
  • The model sometimes takes a product name for a person. Common product names, such as Jira or GitLab, stay text when they stand alone.
  • Services that you set to Cleartext get no replacement.
All limits of name detection in tool responses
  • The container reads strings of at least 12 characters with a space, up to 16,000 characters per response. A name of two or more words that it found is replaced also in the rest of the response. A name of one word is replaced only where the container found it.
  • Fields that your configuration does not scan get no replacement.
  • The container receives the text with the findings of the rules already masked, and no binary data.
  • When the key budget of a user is nearly full, a name gets the fixed mask [name withheld]. A later write cannot restore it. The privacy notice gives the number of withheld names.
  • A very large response can go above internal limits. Names above these limits keep the result of the rules.
  • The licences of the model and the packages are in /licenses in the image.

To judge the result with your own text, use the card Try a text on the admin page for personal data. It runs the two options on the text, marks what each one finds, and shows the time. It comes with a sample text.

The admin card Try a text with the sample text and two result rows. Baseline: 7 found, two mail addresses, two phone numbers, the IBAN, the VPN password and the IP address. Baseline + name detection, marked as your setting: 9 found, the same seven and the persons Jana Petersen and Okafor.
The sample text of the card through the two options. Every value in it belongs to us or to nobody: the mail addresses and the IP address of mcpgate.de, two numbers that the Bundesnetzagentur keeps for films, the example IBAN, and a password that the page makes new on each load. Baseline finds the mail addresses, the phone numbers, the IBAN, the IP address and the password. Name detection adds persons, and its finds can change from run to run: in this run it found Jana Petersen and Okafor. Nobody marks the company, the city or the ticket number.

Three setups

  • Without the NER container (rules and spaCy): 2 vCPU, 4 GB RAM, any CPU.
  • NER on the same host (gateway, Redis and ner): 4 vCPU with VNNI or dotprod, 8 GB RAM. Limit the NER container to 2 or 3 CPUs, so that a burst of texts cannot slow the gateway.
  • NER on its own host: the gateway on 2 vCPU and 4 GB RAM. The NER host on 2 to 4 vCPU with VNNI or dotprod, and 2 to 4 GB RAM. Our own production runs this way.

Warning: connect the NER container only through a private network or a VPN, never from the internet. Set the same NER_CLIENT_SECRET on the container and on the gateway. Then the container processes text only for a request with this secret. Without the secret, it answers every request that reaches it. /health and /info answer without the secret and process no text.

The CPU decides the speed

The NER container reads one text per CPU core. More cores do not make one text faster. They let more texts run at the same time.

If the CPU has int8 dot-product instructions, the container uses a smaller int8 model. On our test server it is 2.2 to 2.4 times as fast as fp32 and finds persons almost as well. These instructions are VNNI on x86 (AMD from Zen 4, Intel Xeon from Cascade Lake) and dotprod on ARM. The container checks the CPU when it starts, and it picks int8 or fp32 by itself. To check a server:

grep -oE 'avx512_vnni|avx_vnni|asimddp' /proc/cpuinfo | sort -u

If the output is empty, the container uses fp32. Without these instructions, int8 is only about 10 % faster and finds fewer names. fp32 works, but it is slower. Then use a separate NER host with 4 cores.

Measured on 31 October 2026

One server with AMD EPYC Zen 5 (VNNI). Each backend on 1 core, 40 texts per size. The times include the HTTP call for the NER container; spaCy runs in the gateway process. spaCy is the German model that the gateway image ships.

Time per text: int8 needs less than half the time of fp32Median per text, one core per backend, 40 texts per size. Each column has its own scale. Shorter is faster.
Tool100 characters500 characters2,000 characters
spaCyNER fp32NER int8 spaCy: 8 ms NER fp32: 29 ms NER int8: 13 ms spaCy: 18 ms NER fp32: 92 ms NER int8: 38 ms spaCy: 47 ms NER fp32: 349 ms NER int8: 143 ms

Rule of thumb for int8 with VNNI: about 70 ms per 1,000 characters and core.

The numbers of this chart
Tool100 characters500 characters2,000 characters
spaCy8 ms18 ms47 ms
NER fp3229 ms92 ms349 ms
NER int813 ms38 ms143 ms
Persons found: the NER container is clearly better than spaCyTest split of GermEval 2014, 5,100 German sentences. Person F1 from 0 to 1: longer is better. Time: shorter is faster.
ToolPerson F1, exactPerson F1, overlapTime per 1,000 characters
spaCyNER fp32NER int8 spaCy: 0.77 NER fp32: 0.88 NER int8: 0.86 spaCy: 0.84 NER fp32: 0.92 NER int8: 0.90 spaCy: 24 ms NER fp32: 274 ms NER int8: 124 ms
The numbers of this chart
ToolPerson F1, exactPerson F1, overlapTime per 1,000 characters
spaCy0.770.8424 ms
NER fp320.880.92274 ms
NER int80.860.90124 ms

The NER container finds persons clearly better than spaCy. spaCy is faster. RAM: about 250 MB (int8) or about 700 MB (fp32). A limit of 1 GB is safe. On disk, the image takes 1 to 2 GB. In our own production, the NER container runs on a Zen 4 CPU with int8. With one CPU core and without the network, a text of 500 characters took about 26 ms on a Zen 4 desktop CPU (measured on 31 October 2026).

Which servers have these instructions

Start it

On the same host, in ~/.mcpgate:

docker compose --profile ner up -d

Then add this line to ~/.mcpgate/.env, and run docker compose up -d again. The NER container has this fixed address on its own Docker network. The gateway accepts an IP address only, not a host name.

NER_SERVICE_URL=http://172.31.250.2:8765/

On its own host, publish the port on the private address only:

docker run -d --restart unless-stopped --cpus 4 -p <private-ip>:8765:8765 mcpgate/ner:v2.0.3584

Then write NER_SERVICE_URL=http://<private-ip>:8765/ into ~/.mcpgate/.env on the gateway host.

The same container also works with maisecrets on your own computer, as its remote_detector.