Skip to main content

Deploy Custom MCP Servers

Runlayer Deploy enables you to deploy custom MCP servers to managed infrastructure with automatic scaling, monitoring, and HTTPS endpoints. When to use Runlayer Deploy:
  • Deploy custom MCP servers not available in the catalog
  • 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)
  • Admin Permissions in the Runlayer platform
  • A Dockerfile and application code ready to deploy

Quick Start

1

Initialize Deployment

Create a new deployment and generate configuration:
uvx runlayer deploy init --secret <admin-key> --host <runlayer-url>
This creates a deployment in the backend and generates a runlayer.yaml template with your deployment ID.
2

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
3

Deploy

Build and deploy your service:
uvx runlayer deploy --config runlayer.yaml --secret <admin-key> --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
  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.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.
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)
  • $$RUNLAYER_BASE_URL - Special backend variable - your deployment’s public URL (backend replaces at runtime)
$$RUNLAYER_BASE_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: $$RUNLAYER_BASE_URL/callback   # Backend replaces with deployment URL
Loading variables:
# From environment
export DATABASE_URL=postgres://localhost/db
export API_KEY=secret123
uvx runlayer deploy --secret <admin-key> --host <runlayer-url>

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

# From specific .env file
uvx runlayer deploy --secret <admin-key> --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 variable into your container:
  • RUNLAYER_BASE_URL - Your deployment’s public URL: https://your-platform.com/api/v1/proxy/hosted/{deployment_id}
This allows your service to know its own public endpoint and construct URLs that route back through Runlayer’s infrastructure. Backend Placeholder Replacement: Use $$RUNLAYER_BASE_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 $$RUNLAYER_BASE_URL with actual deployment URL
  PUBLIC_ENDPOINT: "$$RUNLAYER_BASE_URL/public/api"
At runtime, this becomes:
  • PUBLIC_ENDPOINT: https://your-platform.com/api/v1/proxy/hosted/{deployment_id}/public/api
Important: Use double $$ ($$RUNLAYER_BASE_URL), not single $.

CLI Commands

deploy init

Initialize a new deployment and generate configuration file.
uvx runlayer deploy init --secret <admin-key> --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: Admin API key (required)
  • --host, -H: Runlayer instance URL (required)

deploy

Build and deploy your service.
uvx runlayer deploy --config runlayer.yaml --secret <admin-key> --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
Options:
  • --config, -c: Path to config file (default: runlayer.yaml)
  • --secret, -s: Admin API key (required)
  • --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 --secret <admin-key> --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: Admin API key (required)
  • --host, -H: Runlayer instance URL (required)
  • --env-file, -e: Path to .env file for variable substitution (optional, auto-discovers .env by default)

deploy destroy

Teardown deployment and destroy infrastructure.
uvx runlayer deploy destroy --config runlayer.yaml --secret <admin-key> --host <runlayer-url>
This action is destructive and cannot be undone. All infrastructure resources will be permanently deleted. You must disconnect any MCP servers using this deployment before destroying it.
Options:
  • --config, -c: Path to config file containing deployment ID
  • --deployment-id, -d: Deployment ID (overrides config file)
  • --secret, -s: Admin API key (required)
  • --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: $$RUNLAYER_BASE_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 --secret <admin-key> --host <runlayer-url>

# Or specify explicitly
uvx runlayer deploy --secret <admin-key> --host <runlayer-url> --env-file .env.production
Timing matters: CLI substitution (${VAR}) happens before sending to backend. Backend substitution ($$RUNLAYER_BASE_URL) happens at deployment time. This means validation errors will show CLI-substituted values, but $$RUNLAYER_BASE_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 your catalog:
  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 an MCP server in your catalog

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:Disconnect all MCP servers using this deployment first. Go to the deployment detail page to view connected servers, then delete each server before destroying the deployment.
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.
  • $$RUNLAYER_BASE_URL getting substituted by CLI: Use double dollar sign ($$RUNLAYER_BASE_URL), not single (${RUNLAYER_BASE_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

Next Steps