Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.runlayer.com/llms.txt

Use this file to discover all available pages before exploring further.

Deploy Custom MCP Servers

Runlayer Deploy enables you to deploy custom MCP servers to managed infrastructure with automatic scaling, monitoring, and HTTPS endpoints.
Want to build and deploy your MCP server automatically with your agent? Start with Building and Deploying Custom MCPs. That flow uses Runlayer MCP + MCP Builder on top of Runlayer Deploy.
When to use Runlayer Deploy:
  • Deploy custom MCP servers not already available as connectors
  • Build internal or proprietary integrations
  • Run services requiring specific dependencies or configurations

Prerequisites

Before deploying, ensure you have:
  • Docker installed and running locally
  • Runlayer CLI installed (uvx runlayer)
  • A Dockerfile and application code ready to deploy
Non-admin users can request deploy-based MCP servers through the connectors flow. The request follows the standard approval workflow. Direct deployment via the CLI requires admin permissions.

Quick Start

1

Log in to Runlayer

Authenticate the CLI with your Runlayer instance:
uvx runlayer login --host <runlayer-url>
2

Initialize Deployment

Create a new deployment and generate configuration:
uvx runlayer deploy init --host <runlayer-url>
This creates a deployment in the backend and generates a runlayer.yaml template with your deployment ID. You can also pick a custom icon for the deployment during creation or update it later from the deployment settings dialog.
3

Configure Service

Edit the generated runlayer.yaml file:
  • Set your service port
  • Choose CPU and memory resources
  • Add environment variables
  • Configure build settings for your Dockerfile
4

Deploy

Build and deploy your service:
uvx runlayer deploy --config runlayer.yaml --host <runlayer-url>

Runtimes

Docker

The Docker runtime builds your application locally using Docker and deploys it as a container. This gives you full control over dependencies and supports any language or framework. Build Configuration:
  • dockerfile: Path to your Dockerfile (default: Dockerfile)
  • context: Build context directory (default: .)
  • platform: CPU architecture - x86 or arm (default: arm, recommended for cost savings)
  • args: Build arguments as key-value pairs
  • target: Target stage for multi-stage builds (optional)

Coming Soon

Managed Runtimes for Python and TypeScript—deploy with just your code and dependencies.

Configuration Reference

Complete runlayer.yaml structure:
id: <uuid>              # Auto-generated, do not modify
name: my-service        # Lowercase, URL-friendly name
runtime: docker         # Currently only "docker" is supported

build:
  dockerfile: Dockerfile
  context: .
  platform: arm         # "arm" or "x86" (arm recommended)
  target: null          # Optional: multi-stage build target
  args: {}              # Build arguments (key-value pairs)

service:
  port: 8000           # Container port your app listens on
  path: /mcp           # URL path prefix (optional)
  expose: []           # Public path patterns (optional)

infrastructure:
  cpu: 512             # CPU units
  memory: 1024         # Memory in MB
  platform: arm        # CPU architecture (must match build.platform)
  enable_db: false     # Enable database storage

env: {}                # Environment variables (key-value pairs)

Field Details

service.expose Make specific paths publicly accessible without authentication. Supports wildcard patterns. Examples:
  • ['*'] - Expose all paths
  • ['/public/*'] - Expose all paths under /public
  • ['/api/v1/*', '/health'] - Expose API v1 endpoints and health check
Exposed paths bypass authentication. Only expose paths that are designed to be public.
infrastructure Valid CPU and memory combinations:
CPU (vCPU)Memory Options (MB)Best For
256 (.25 vCPU)512, 1024, 2048Minimal services
512 (.5 vCPU)1024, 2048, 3072, 4096Light services
1024 (1 vCPU)2048, 3072, 4096, 5120, 6144, 7168, 8192Standard services
2048 (2 vCPU)4096-16384 (1024 MB increments)Medium workloads
4096 (4 vCPU)8192-30720 (1024 MB increments)Heavy workloads
See AWS Fargate pricing for cost estimates.
infrastructure.platform CPU architecture for the container runtime: x86 or arm.
  • In build mode: Defaults to build.platform. If explicitly set, must match build.platform or validation will fail.
  • In image mode: Defaults to x86 if not specified. Set this to match the architecture of your pre-built Docker image.
infrastructure.enable_db Enable a managed database for persistent storage. When enabled, a DynamoDB table is provisioned with the following environment variables injected into your container:
  • DB_TABLE_NAME - The DynamoDB table name
  • DB_TABLE_REGION - The AWS region of the table
The table uses a partition key (pk) and sort key (sk) schema. TTL is enabled via the ttl attribute (set to a Unix timestamp to auto-expire items). Use any standard DynamoDB client to interact with the table—boto3 (Python), AWS SDK (Node.js), or any AWS SDK. All standard DynamoDB operations are supported: GetItem, PutItem, Query, Scan, BatchGetItem, BatchWriteItem, etc. Example MCP tool with persistence (Python):
import os
import boto3
from typing import Any

dynamodb = boto3.resource("dynamodb", region_name=os.environ.get("DB_TABLE_REGION"))

@mcp.tool
def add_item(pk: str, sk: str, data: dict[str, Any]) -> dict:
    """Add an item to the database."""
    table_name = os.environ.get("DB_TABLE_NAME")
    if not table_name:
        return {"error": "DB_TABLE_NAME not set"}

    table = dynamodb.Table(table_name)
    item = {"pk": pk, "sk": sk, **data}

    try:
        table.put_item(Item=item)
        return {"success": True, "item": item}
    except Exception as e:
        return {"success": False, "error": str(e)}
Irreversible: Once enable_db is set to true and deployed, it cannot be disabled. Attempting to set it back to false will fail validation to prevent data loss.
Building a deployed server that needs OAuth with a third-party service (e.g., Slack, Google)? See the OAuth for Deployed MCP Servers guide for the dual-auth architecture and token storage patterns.
env Environment variables are passed to your container at runtime. Values are automatically masked as *** in stored configurations and API responses for security. CLI Variable Substitution: The CLI supports shell-style environment variable substitution when loading runlayer.yaml, allowing you to reference local environment variables or .env files without hardcoding sensitive values. Syntax:
  • ${VAR} - Required variable (deployment fails if not set)
  • ${VAR:-default} - Use default value if variable is unset or empty
  • ${VAR-default} - Use default value only if variable is unset (not if empty)
  • $$DEPLOYMENT_URL - Special backend variable - your deployment’s public URL (backend replaces at runtime)
  • $$RUNLAYER_URL - Special backend variable - the Runlayer platform URL
  • $$RUNLAYER_OAUTH_CALLBACK_URL - Special backend variable - OAuth callback URL for integrations
$$DEPLOYMENT_URL is a special variable that resolves to your deployment’s public URL. Use it to construct URLs that reference your service.Remember: Add paths to service.expose to make them publicly accessible without authentication.
Example with CLI substitution:
env:
  DATABASE_URL: ${DATABASE_URL}                # Required from environment
  API_KEY: ${API_KEY}                          # Required from environment
  LOG_LEVEL: ${LOG_LEVEL:-info}                # Defaults to 'info'
  DEBUG: ${DEBUG:-false}                       # Defaults to 'false'
  CALLBACK_URL: $$DEPLOYMENT_URL/callback   # Backend replaces with deployment URL
Loading variables:
# From environment
export DATABASE_URL=postgres://localhost/db
export API_KEY=secret123
uvx runlayer deploy --host <runlayer-url>

# From .env file (auto-discovered)
# Place .env next to runlayer.yaml or in current directory
uvx runlayer deploy --host <runlayer-url>

# From specific .env file
uvx runlayer deploy --host <runlayer-url> --env-file .env.prod
Auto-discovery: The CLI automatically searches for .env in your current working directory. Automatic Environment Variables: The backend automatically injects the following variables into your container:
  • DEPLOYMENT_URL - Your deployment’s public URL: https://your-platform.com/api/v1/proxy/hosted/{deployment_id}
  • RUNLAYER_URL - The Runlayer platform URL
  • RUNLAYER_OAUTH_CALLBACK_URL - OAuth callback URL: https://your-platform.com/oauth/callback
This allows your service to know its own public endpoint and construct URLs that route back through Runlayer’s infrastructure. Backend Placeholder Replacement: Use $$DEPLOYMENT_URL (double dollar sign) as a placeholder in your YAML configuration. The backend replaces this at deployment time with your deployment’s actual public URL. Example:
service:
  port: 8000
  expose:
    - "/public/*"        # Expose public endpoints

env:
  # Backend replaces $$DEPLOYMENT_URL with actual deployment URL
  PUBLIC_ENDPOINT: "$$DEPLOYMENT_URL/public/api"
At runtime, this becomes:
  • PUBLIC_ENDPOINT: https://your-platform.com/api/v1/proxy/hosted/{deployment_id}/public/api
Important: Use double $$ ($$DEPLOYMENT_URL), not single $.

CLI Commands

deploy init

Initialize a new deployment and generate configuration file.
uvx runlayer login --host <runlayer-url>
uvx runlayer deploy init --host <runlayer-url>
What it does:
  • Creates a deployment record in your Runlayer instance
  • Generates a runlayer.yaml template with your deployment ID
  • Prompts for deployment name (must be lowercase and URL-friendly)
Options:
  • --config, -c: Path to create config file (default: runlayer.yaml)
  • --secret, -s: API key override (optional; prefer uvx runlayer login)
  • --host, -H: Runlayer instance URL (required)

deploy

Build and deploy your service.
uvx runlayer deploy --config runlayer.yaml --host <runlayer-url>
What it does:
  1. Loads environment variables from .env (auto-discovered) or --env-file
  2. Substitutes variables in your YAML configuration
  3. Validates your YAML configuration
  4. Builds Docker image locally using your Dockerfile
  5. Pushes image to managed registry
  6. Deploys infrastructure
  7. Monitors deployment with live log streaming
  8. Prints the deployment URL so you can open it directly
Options:
  • --config, -c: Path to config file (default: runlayer.yaml)
  • --secret, -s: API key override (optional; prefer uvx runlayer login)
  • --host, -H: Runlayer instance URL (required)
  • --env-file, -e: Path to .env file for variable substitution (optional, auto-discovers .env by default)

deploy validate

Validate configuration without deploying.
uvx runlayer deploy validate --config runlayer.yaml --host <runlayer-url>
Checks YAML syntax and validates against backend schema. Useful for CI/CD pipelines or pre-deployment validation. Options:
  • --config, -c: Path to config file (default: runlayer.yaml)
  • --secret, -s: API key override (optional; prefer uvx runlayer login)
  • --host, -H: Runlayer instance URL (required)
  • --env-file, -e: Path to .env file for variable substitution (optional, auto-discovers .env by default)

deploy pull

Fetch the current deployment configuration from the backend and save it as a local runlayer.yaml.
uvx runlayer deploy pull --host <runlayer-url>
What it does:
  1. Reads the deployment ID from the existing runlayer.yaml (or --deployment-id)
  2. Creates a timestamped backup of the current file (if present)
  3. Downloads the latest configuration and writes it locally
Environment variables appear as ${VAR_NAME} placeholders; backend variables like $$DEPLOYMENT_URL are preserved as-is. Options:
  • --config, -c: Path to save config file (default: runlayer.yaml)
  • --deployment-id, -d: Deployment ID (overrides config file)
  • --secret, -s: API key override (optional; prefer uvx runlayer login)
  • --host, -H: Runlayer instance URL (required)

deploy destroy

Teardown deployment and destroy infrastructure.
uvx runlayer deploy destroy --config runlayer.yaml --host <runlayer-url>
This action is destructive and cannot be undone. All infrastructure resources will be permanently deleted.
If the deployment has connected MCP servers, the UI will show them and offer a Delete All option that removes the deployment and its connected servers in one step (requires permission to delete those servers). Without permission, you must ask the connector owner to disconnect them first. Options:
  • --config, -c: Path to config file containing deployment ID
  • --deployment-id, -d: Deployment ID (overrides config file)
  • --secret, -s: API key override (optional; prefer uvx runlayer login)
  • --host, -H: Runlayer instance URL (required)

Examples

Example: Multi-stage Node.js Build with legacy SSE transport

TypeScript MCP server with build arguments and optimized production build:
name: custom-mcp
runtime: docker

build:
  dockerfile: Dockerfile
  context: .
  platform: x86
  target: production
  args:
    NODE_VERSION: "20"

service:
  port: 3000
  path: /sse

infrastructure:
  cpu: 1024
  memory: 2048

env:
  NODE_ENV: "production"

Example: Environment Variable Substitution

Production service with secrets loaded from .env file: runlayer.yaml:
name: production-api
runtime: docker

build:
  dockerfile: Dockerfile
  context: .
  platform: arm

service:
  port: 8000
  path: /api
  expose:
    - "/public/*"         # Expose public endpoints

infrastructure:
  cpu: 1024
  memory: 2048

env:
  # Required variables (CLI substitutes from .env file)
  DATABASE_URL: ${DATABASE_URL}
  API_KEY: ${API_KEY}
  JWT_SECRET: ${JWT_SECRET}
  
  # Variables with defaults (CLI substitutes with fallback)
  LOG_LEVEL: ${LOG_LEVEL:-info}
  DEBUG: ${DEBUG:-false}
  WORKERS: ${WORKERS:-4}
  
  # Special backend variable (uses exposed endpoints)
  PUBLIC_API_URL: $$DEPLOYMENT_URL/public/endpoint
.env:
DATABASE_URL=postgres://prod-db.example.com:5432/myapp
API_KEY=prod_key_12345
JWT_SECRET=super_secret_jwt_key
LOG_LEVEL=warn
WORKERS=8
Deploy:
# Uses .env automatically (same directory as runlayer.yaml)
uvx runlayer deploy --host <runlayer-url>

# Or specify explicitly
uvx runlayer deploy --host <runlayer-url> --env-file .env.production
Timing matters: CLI substitution (${VAR}) happens before sending to backend. Backend substitution ($$DEPLOYMENT_URL) happens at deployment time. This means validation errors will show CLI-substituted values, but $$DEPLOYMENT_URL remains as-is until the backend deploys your service.

Example: Service with Public Webhooks

Slack integration with publicly exposed webhook endpoints:
name: slack-mcp
runtime: docker

build:
  dockerfile: Dockerfile
  context: .
  platform: arm

service:
  port: 8080
  path: /mcp
  expose:
    - "/webhooks/*"
    - "/.well-known/*"

infrastructure:
  cpu: 512
  memory: 1024

env:
  SLACK_SIGNING_SECRET: "xxx"

Registering as an MCP Server

After your deployment succeeds, register it as an MCP server to make it available in Connectors:
  1. Navigate to the deployment detail page in the Runlayer UI
  2. Click “Register as MCP Server” and update any information you need
  3. Click on “Create MCP”
  4. Your deployed service is now available as a connector in Runlayer
Connected MCP servers appear in the Connectors tab on the deployment detail page with per-server tool counts.

Identity Forward

When enabled on a deployed MCP server, the Runlayer proxy injects the authenticated caller’s identity into upstream HTTP requests via X-Runlayer-* headers. Your server reads them like any other header — no token verification, no JWKS, no SDK — to scope data per user, write meaningful audit logs, enforce per-user rate limits, or personalize responses.
  • Available only for MCP servers deployed through Runlayer Deploy. The trust model relies on requests reaching your server only via the Runlayer proxy over managed Deploy networking. Do not expose your deployed server to the public internet outside the Deploy ingress, or anyone can spoof these headers.
  • Headers are plain HTTP, not signed tokens. Signed identity is planned for a future release.
  • Identity headers are NOT a substitute for authorization — enforce permissions in Runlayer policies and your server logic.
  • Runlayer overwrites the X-Runlayer-* namespace after any admin-configured headers are merged, so admins cannot spoof or mask identity via the server’s headers config.
  • Header values are never written to Runlayer proxy request logs (only a boolean identity_forward_applied and the subject type are logged).
  • Off by default. Admin-only to toggle. Enabling sends end-user emails / agent identifiers to the deployed server — accept responsibility per your privacy policy.

Headers sent

HeaderDescriptionSent for
X-Runlayer-Subject-TypeOne of user, agent, oboalways
X-Runlayer-Org-IdRunlayer organization UUIDalways
X-Runlayer-User-EmailEnd-user emailuser, obo
X-Runlayer-User-IdRunlayer user UUIDuser, obo
X-Runlayer-Agent-IdAgent account UUIDagent, obo
X-Runlayer-Agent-NameAgent display nameagent, obo

Subject types

Subject typeWhen it occurs
userA human end-user calling directly through their MCP client (Cursor, Claude Desktop, etc.).
agentA standalone agent account (machine-to-machine) calling with its own credentials. No human in the loop.
oboAn agent calling on behalf of a delegating user. Both agent identity and the delegator’s user identity are forwarded.

Example requests

Human user:
POST /mcp HTTP/1.1
Host: my-server.deploy.runlayer.com
X-Runlayer-Subject-Type: user
X-Runlayer-Org-Id: 9f3a...
X-Runlayer-User-Email: jane@acme.com
X-Runlayer-User-Id: 7b1c...
Standalone agent (m2m):
POST /mcp HTTP/1.1
Host: my-server.deploy.runlayer.com
X-Runlayer-Subject-Type: agent
X-Runlayer-Org-Id: 9f3a...
X-Runlayer-Agent-Id: a4d2...
X-Runlayer-Agent-Name: nightly-reporter
Agent on-behalf-of a user (OBO):
POST /mcp HTTP/1.1
Host: my-server.deploy.runlayer.com
X-Runlayer-Subject-Type: obo
X-Runlayer-Org-Id: 9f3a...
X-Runlayer-User-Email: jane@acme.com
X-Runlayer-User-Id: 7b1c...
X-Runlayer-Agent-Id: a4d2...
X-Runlayer-Agent-Name: jane-research-agent

Enable

  1. Open the server’s configuration in Runlayer (admin only).
  2. Toggle Forward authenticated user identity in request headers on.
  3. Save. A server.identity_forward.enabled audit event is emitted.

Reading the headers

Python (FastAPI / FastMCP):
from fastapi import Request, HTTPException


def get_caller(req: Request) -> dict:
    subject_type = req.headers.get("x-runlayer-subject-type")
    if subject_type is None:
        raise HTTPException(401, "missing identity")

    return {
        "subject_type": subject_type,
        "org_id": req.headers.get("x-runlayer-org-id"),
        "user_email": req.headers.get("x-runlayer-user-email"),
        "user_id": req.headers.get("x-runlayer-user-id"),
        "agent_id": req.headers.get("x-runlayer-agent-id"),
        "agent_name": req.headers.get("x-runlayer-agent-name"),
    }
TypeScript (Express):
function getCaller(req: Request) {
  const subjectType = req.header("x-runlayer-subject-type");
  if (!subjectType) throw new Error("missing identity");

  return {
    subjectType,
    orgId: req.header("x-runlayer-org-id"),
    userEmail: req.header("x-runlayer-user-email"),
    userId: req.header("x-runlayer-user-id"),
    agentId: req.header("x-runlayer-agent-id"),
    agentName: req.header("x-runlayer-agent-name"),
  };
}

v1 limitations

  • Transports: streaming-http and sse only. Not supported on stdio (use environment variables / placeholders if you need identity for stdio).
  • No signed tokens — header-based, network-trust mechanism. Cryptographic identity (signed JWT + JWKS) is planned for a future release and will be required before this feature can extend to non-Deploy MCP servers.
  • Email-only personalization. Organization roles, groups, and custom claims are not forwarded.
  • Runlayer self-MCP (runlayer-mcp) does not receive these headers; it already runs in-process with full subject context.

Troubleshooting

Solutions:
  • Test build locally first: docker build -t test .
  • Check Dockerfile syntax and instructions
  • Verify build context includes all required files
  • Check build arguments are passed correctly
Solution:Refer to the configuration reference table for valid combinations. Not all CPU/memory pairs are supported. For example, 256 CPU only supports 512, 1024, or 2048 MB memory.
Solutions:Check deployment logs in the web UI. Common issues:
  • Container failing health checks (check your app responds on the configured port)
  • Wrong port configured in YAML
  • Missing required environment variables
  • Application crashes at startup (check application logs)
Solution:If the deployment has connected MCP servers, use the Delete All option to remove the deployment and its servers together. If you lack permission to delete those servers, ask the connector owner or an administrator to do it.
Explanation:Values are automatically masked as *** in stored configurations and API responses for security. This is expected behavior. Your actual values are securely passed to the container at runtime and are not visible in the UI or API.
Common issues:
  • “Required environment variable ‘VAR’ is not set”: Variable isn’t in your environment or .env file. Check spelling and ensure it’s exported.
  • Wrong default syntax: Use ${VAR:-default} (with colon) to handle empty strings, or ${VAR-default} (no colon) to only use default if unset.
  • $$DEPLOYMENT_URL getting substituted by CLI: Use double dollar sign ($$DEPLOYMENT_URL), not single (${DEPLOYMENT_URL}). Single dollar triggers CLI substitution, double dollar preserves it for backend replacement.
Solutions:
  • Verify platform architecture matches (x86 vs arm)
  • Check all dependencies are in Dockerfile, not just on your local machine
  • Ensure build arguments are specified in YAML if required
  • Review build logs for missing packages or permission errors
Checklist:
  • Identity Forward toggle is on for this server (admin server config).
  • The MCP server is backed by a Runlayer Deploy deployment — the toggle is hidden / inactive for self-hosted servers.
  • Transport is streaming-http or sse. stdio is not supported in v1.
  • Caller has identity (logged-in user, OBO agent, or standalone agent account); anonymous calls forward only Subject-Type and Org-Id.
  • You’re reading the headers case-insensitively (HTTP header names are not case-sensitive; httpx canonicalizes them on send).
exec /usr/local/bin/python: exec format error
Cause: The Docker image architecture doesn’t match infrastructure.platform.Fix: Set infrastructure.platform to match your docker image architecture.
  • For x86/amd64 images: platform: x86
  • For ARM images: platform: arm

Next Steps

Custom MCP Servers

Learn best practices for building custom MCP servers

OAuth for Deployed Servers

Dual-auth architecture and token storage for OAuth connectors

Connectors

View and manage your connectors

Security Best Practices

Secure your MCP deployments and integrations