NitroStack Logo
/python
/deployment
/cloud

Cloud Platform Deployment (Python)

Time to read: 15 minutes

Deploy your Python MCP server to NitroCloud or to AWS, Google Cloud, Azure and others.


The Fastest Way: NitroCloud

Why choose NitroCloud?

Skip the setup complexity. NitroCloud is purpose-built for MCP servers and detects NitroStack Python projects automatically:

  • One-step deployment: connect your code and it builds and runs the container for you
  • Auto-scaling: scales with traffic
  • Built-in monitoring: metrics, logs and alerts
  • Automatic SSL: free HTTPS certificates
  • Environment variables: set secrets in the dashboard, never in your code
  • Free tier: no credit card required

What NitroCloud Needs From Your Project

NitroCloud recognizes a Python NitroStack app when nitrostack appears in pyproject.toml (or requirements.txt) or your code imports it. Make sure your project has:

RequirementWhy
pyproject.toml at the project root (or one folder deep)Dependencies are installed with uv. A uv.lock is used when present
nitrostack listed as a dependencyLets NitroCloud pick the Python build
main.py as the entry pointThe container runs python main.py
MCP_TRANSPORT_TYPE=http supportThe container sets HOST=0.0.0.0, PORT=3000 and MCP_TRANSPORT_TYPE=http for you

No Dockerfile is needed. NitroCloud supplies its own.

Quick Start

Bash
# 1. Check the project locally
nitrostack-py validate

# 2. Make sure it runs over HTTP
MCP_TRANSPORT_TYPE=http nitrostack-py start

# 3. Push to GitHub
git add . && git commit -m "Ready to deploy" && git push

Then in the NitroCloud dashboard:

  1. Sign in and choose Create NitroApp
  2. Connect GitHub and pick your repository and branch
  3. Select the project folder (the one holding pyproject.toml)
  4. Add environment variables, for example API_KEY, JWT_SECRET and any OAuth settings
  5. Click Deploy

Your MCP server is live at its NitroCloud URL. Point your MCP client at https://<your-app-url>/mcp.

nitrostack-py does not currently ship login or deploy commands. Deployment to NitroCloud happens from the dashboard, not the CLI.

Updating Your Server

Push to the connected branch and redeploy from the dashboard. To change configuration, edit the environment variables and redeploy. Never commit .env.

Verify the Deployment

Bash
curl https://<your-app-url>/mcp/health

Try NitroCloud Free →


Self-Hosting Options

If you prefer to manage your own infrastructure, here are the popular alternatives. Every example below runs the same container from the Docker guide:

Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir nitrostack && pip install --no-cache-dir -r requirements.txt
ENV HOST=0.0.0.0 MCP_TRANSPORT_TYPE=http PORT=3000
EXPOSE 3000
CMD ["nitrostack-py", "start", "--port", "3000"]

Common rules for every platform:

  • Bind HOST=0.0.0.0 and the platform PORT
  • Set MCP_TRANSPORT_TYPE=http (use dual only if you also need stdio in the same process, which is unusual in the cloud)
  • Health check: GET /mcp/health
  • MCP endpoint: POST/GET/DELETE /mcp, with no trailing slash
  • Keep API_KEY, JWT_SECRET and OAuth settings in the platform secret store

What You'll Learn

  • AWS deployment
  • Google Cloud deployment
  • Azure deployment
  • Platform comparison
  • Secrets, logging and CI/CD

AWS Deployment

AWS ECS (Elastic Container Service)

1. Create an ECR repository

Bash
aws ecr create-repository --repository-name my-mcp-server

2. Build and push

Bash
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com

docker build -t my-mcp-server .
docker tag my-mcp-server:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/my-mcp-server:latest
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/my-mcp-server:latest

3. Task definition

JSON
{
  "family": "mcp-server",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "containerDefinitions": [
    {
      "name": "mcp-server",
      "image": "<account-id>.dkr.ecr.us-east-1.amazonaws.com/my-mcp-server:latest",
      "portMappings": [{ "containerPort": 3000 }],
      "environment": [
        { "name": "HOST", "value": "0.0.0.0" },
        { "name": "MCP_TRANSPORT_TYPE", "value": "http" },
        { "name": "PORT", "value": "3000" }
      ],
      "secrets": [
        {
          "name": "JWT_SECRET",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:<account-id>:secret:mcp/jwt-secret"
        }
      ]
    }
  ]
}

4. Create the service

Bash
aws ecs create-service \
  --cluster mcp-cluster \
  --service-name mcp-server \
  --task-definition mcp-server \
  --desired-count 2 \
  --launch-type FARGATE

Point the load balancer health check at /mcp/health.

AWS Lambda (Serverless)

Lambda works for stateless HTTP use, but MCP sessions are stateful, so prefer ECS or Cloud Run for long-lived sessions. If you still want Lambda:

Bash
nitrostack-py pack
pip install dist/*.whl -t package/

Use a container image deployment with the Lambda Web Adapter so the same nitrostack-py start process serves requests:

Dockerfile
FROM public.ecr.aws/docker/library/python:3.12-slim
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir nitrostack -r requirements.txt
ENV HOST=0.0.0.0 PORT=8080 MCP_TRANSPORT_TYPE=http
CMD ["nitrostack-py", "start", "--port", "8080"]

Google Cloud Platform

Cloud Run

Bash
# Enable APIs
gcloud services enable run.googleapis.com cloudbuild.googleapis.com

# Build with Cloud Build
gcloud builds submit --tag gcr.io/PROJECT_ID/my-mcp-server

# Deploy
gcloud run deploy my-mcp-server \
  --image gcr.io/PROJECT_ID/my-mcp-server \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars HOST=0.0.0.0,MCP_TRANSPORT_TYPE=http \
  --set-secrets JWT_SECRET=jwt-secret:latest

Cloud Run injects PORT, and nitrostack-py start reads it from the environment. Remove --allow-unauthenticated and use Cloud Run IAM if the server is private.

GKE (Kubernetes)

Yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-server
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mcp-server
  template:
    metadata:
      labels:
        app: mcp-server
    spec:
      containers:
        - name: mcp-server
          image: gcr.io/PROJECT_ID/my-mcp-server:latest
          ports:
            - containerPort: 3000
          env:
            - name: HOST
              value: "0.0.0.0"
            - name: MCP_TRANSPORT_TYPE
              value: "http"
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: mcp-secrets
                  key: jwt-secret
          readinessProbe:
            httpGet:
              path: /mcp/health
              port: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: mcp-server
spec:
  type: LoadBalancer
  selector:
    app: mcp-server
  ports:
    - port: 80
      targetPort: 3000
Bash
kubectl apply -f deployment.yaml

Microsoft Azure

Azure Container Instances

Bash
# Create resource group
az group create --name mcp-rg --location eastus

# Create container
az container create \
  --resource-group mcp-rg \
  --name my-mcp-server \
  --image <registry>/my-mcp-server:latest \
  --ports 3000 \
  --environment-variables HOST=0.0.0.0 MCP_TRANSPORT_TYPE=http PORT=3000 \
  --secure-environment-variables JWT_SECRET=$JWT_SECRET

Azure Kubernetes Service (AKS)

Bash
# Create AKS cluster
az aks create --resource-group mcp-rg --name mcp-aks --node-count 2

# Get credentials
az aks get-credentials --resource-group mcp-rg --name mcp-aks

# Deploy (reuse the manifest from the GKE section, with your registry image)
kubectl apply -f deployment.yaml

DigitalOcean

App Platform

Yaml
# .do/app.yaml
name: my-mcp-server
services:
  - name: web
    github:
      repo: your-org/your-repo
      branch: main
    dockerfile_path: Dockerfile
    http_port: 3000
    envs:
      - key: HOST
        value: 0.0.0.0
      - key: MCP_TRANSPORT_TYPE
        value: http
      - key: JWT_SECRET
        value: ${JWT_SECRET}
        type: SECRET
    health_check:
      http_path: /mcp/health

Heroku

Bash
# Create app
heroku create my-mcp-server
heroku stack:set container

# heroku.yml
# build:
#   docker:
#     web: Dockerfile

# Config
heroku config:set MCP_TRANSPORT_TYPE=http HOST=0.0.0.0 JWT_SECRET=...

# Deploy
git push heroku main

# Scale
heroku ps:scale web=1

Heroku sets PORT for you.

Platform Comparison

PlatformBest ForCostScaling
NitroCloudMCP servers, fastest setup$Automatic
AWS ECSEnterprise$$$Excellent
AWS LambdaStateless, event-driven$Automatic
Google Cloud RunServerless containers$$Automatic
GKE/AKSLarge scale$$$Manual
DigitalOceanSimplicity$$Manual
HerokuQuick deploy$$Easy

Environment Configuration

Secrets Management

AWS Secrets Manager:

Python
import json
import boto3

def get_secret(secret_name: str) -> dict:
    client = boto3.client("secretsmanager")
    response = client.get_secret_value(SecretId=secret_name)
    return json.loads(response["SecretString"])

Google Cloud Secret Manager:

Python
from google.cloud import secretmanager

def get_secret(project_id: str, name: str) -> str:
    client = secretmanager.SecretManagerServiceClient()
    path = f"projects/{project_id}/secrets/{name}/versions/latest"
    return client.access_secret_version(name=path).payload.data.decode("utf-8")

Prefer injecting secrets as environment variables through the platform (ECS secrets, Cloud Run --set-secrets, Kubernetes secretKeyRef) so your code only reads os.environ.

Monitoring & Logging

CloudWatch (AWS)

On ECS, use the awslogs log driver and your container output goes to CloudWatch with no code changes:

JSON
"logConfiguration": {
  "logDriver": "awslogs",
  "options": {
    "awslogs-group": "/aws/ecs/mcp-server",
    "awslogs-region": "us-east-1",
    "awslogs-stream-prefix": "production"
  }
}

Cloud Logging (GCP)

Cloud Run collects container stdout and stderr automatically. For structured logs:

Python
import json
import sys

def log_entry(message: str, severity: str = "INFO") -> None:
    print(json.dumps({"severity": severity, "message": message}), file=sys.stderr)

Best Practices

  1. Use managed services: less operational overhead
  2. Implement health checks: point them at /mcp/health
  3. Auto-scaling: handle traffic spikes, and set MCP_MAX_SESSIONS per instance
  4. Multi-region: high availability
  5. Secrets management: never hardcode credentials or ship .env
  6. Monitoring & alerts: know when things break
  7. CI/CD pipelines: automate deployments
  8. Cost optimization: right-size CPU and memory

CI/CD Examples

GitHub Actions (AWS)

Yaml
name: Deploy to AWS ECS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Test and validate
        run: |
          pip install nitrostack -r requirements.txt pytest
          pytest
          nitrostack-py validate

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Login to ECR
        id: ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push
        run: |
          docker build -t mcp-server .
          docker tag mcp-server:latest ${{ steps.ecr.outputs.registry }}/mcp-server:latest
          docker push ${{ steps.ecr.outputs.registry }}/mcp-server:latest

      - name: Update ECS service
        run: |
          aws ecs update-service \
            --cluster mcp-cluster \
            --service mcp-server \
            --force-new-deployment

If you deploy with NitroCloud, the CI job only needs to run tests and nitrostack-py validate. Pushing to the connected branch handles the rest.

Next Steps

Master deployment:


← Back: Docker Guide | Next: API Reference →