mcesptool/PRODUCTION_DEPLOYMENT.md
Ryan Malloy 64c1505a00 Add QEMU ESP32 emulation support
Integrate Espressif's QEMU fork for virtual ESP device management:

- QemuManager component with 5 MCP tools (start/stop/list/status/flash)
- Config auto-detects QEMU binaries from ~/.espressif/tools/
- Supports esp32, esp32s2, esp32s3, esp32c3 chip emulation
- Virtual serial over TCP (socket://localhost:PORT) transparent to esptool
- Scan integration: QEMU instances appear in esp_scan_ports results
- Blank flash images initialized to 0xFF (erased NOR flash state)
- 38 unit tests covering lifecycle, port allocation, flash writes
2026-01-28 15:35:22 -07:00

30 KiB

🚀 Production Deployment Guide

Overview

This guide covers deploying the FastMCP ESPTool server in production environments, including containerization, scaling, monitoring, and enterprise integration patterns.

🏭 Production Architecture

Deployment Topology

┌─────────────────────────────────────────────────────────────┐
│                     Production Environment                   │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐     │
│  │   Load      │    │   Reverse   │    │   SSL/TLS   │     │
│  │  Balancer   │───▶│    Proxy    │───▶│ Termination │     │
│  │   (HAProxy) │    │   (Caddy)   │    │   (Cert)    │     │
│  └─────────────┘    └─────────────┘    └─────────────┘     │
│                              │                              │
│  ┌─────────────────────────────────────────────────────────┤
│  │              MCP ESPTool Server Cluster                 │
│  │                                                         │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐    │
│  │  │   Server    │  │   Server    │  │   Server    │    │
│  │  │ Instance 1  │  │ Instance 2  │  │ Instance 3  │    │
│  │  │             │  │             │  │             │    │
│  │  │ Port: 8080  │  │ Port: 8081  │  │ Port: 8082  │    │
│  │  └─────────────┘  └─────────────┘  └─────────────┘    │
│  └─────────────────────────────────────────────────────────┤
│                              │                              │
│  ┌─────────────────────────────────────────────────────────┤
│  │                    Shared Services                      │
│  │                                                         │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐    │
│  │  │   Redis     │  │ PostgreSQL  │  │ Monitoring  │    │
│  │  │   Cache     │  │  Database   │  │  (Grafana)  │    │
│  │  └─────────────┘  └─────────────┘  └─────────────┘    │
│  └─────────────────────────────────────────────────────────┤
│                              │                              │
│  ┌─────────────────────────────────────────────────────────┤
│  │                Hardware Interface                       │
│  │                                                         │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐    │
│  │  │ ESP Device  │  │ ESP Device  │  │ ESP Device  │    │
│  │  │ Station 1   │  │ Station 2   │  │ Station N   │    │
│  │  │             │  │             │  │             │    │
│  │  │/dev/ttyUSB0 │  │/dev/ttyUSB1 │  │/dev/ttyUSBN │    │
│  │  └─────────────┘  └─────────────┘  └─────────────┘    │
│  └─────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────┘

🐳 Container Production Setup

Production Dockerfile

# Multi-stage production Dockerfile
FROM python:3.11-slim-bookworm AS base

# Install system dependencies
RUN apt-get update && apt-get install -y \
    git \
    curl \
    build-essential \
    cmake \
    ninja-build \
    libusb-1.0-0-dev \
    udev \
    && rm -rf /var/lib/apt/lists/*

# Install uv for fast package management
RUN pip install uv

# Create non-root user for security
RUN groupadd -r esptool && useradd -r -g esptool -d /app -s /bin/bash esptool
RUN mkdir -p /app && chown esptool:esptool /app

# Production stage
FROM base AS production

USER esptool
WORKDIR /app

# Copy project files
COPY --chown=esptool:esptool pyproject.toml ./
COPY --chown=esptool:esptool src/ ./src/

# Install production dependencies
RUN uv venv .venv && \
    . .venv/bin/activate && \
    uv pip install -e ".[production]" && \
    uv pip install esptool esp-idf-tools

# Set up ESP-IDF (production minimal)
RUN git clone --depth 1 --branch v5.1 \
    https://github.com/espressif/esp-idf.git /opt/esp-idf && \
    cd /opt/esp-idf && \
    ./install.sh --targets esp32,esp32s3,esp32c3

# Environment setup
ENV ESP_IDF_PATH=/opt/esp-idf
ENV PATH="/opt/esp-idf/tools:/app/.venv/bin:$PATH"
ENV PYTHONPATH=/app/src

# Create directories for data persistence
RUN mkdir -p /app/data /app/logs /app/config

# Health check script
COPY --chown=esptool:esptool scripts/health-check.py ./health-check.py
RUN chmod +x health-check.py

# Security: Remove package management tools in production
USER root
RUN apt-get remove -y git curl build-essential cmake && \
    apt-get autoremove -y && \
    rm -rf /var/lib/apt/lists/*

USER esptool

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
    CMD python health-check.py || exit 1

# Expose port
EXPOSE 8080

# Production startup
CMD ["/app/.venv/bin/python", "-m", "mcp_esptool_server.server", "--production"]

Production Docker Compose

# docker-compose.prod.yml
version: '3.8'

services:
  # Load balancer
  haproxy:
    image: haproxy:2.8-alpine
    ports:
      - "80:80"
      - "443:443"
      - "8404:8404"  # Stats
    volumes:
      - ./config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
      - ./certs:/etc/ssl/certs:ro
    networks:
      - frontend
      - backend
    restart: unless-stopped
    depends_on:
      - esptool-server-1
      - esptool-server-2
      - esptool-server-3

  # MCP ESPTool Server Instances
  esptool-server-1: &esptool-server
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    image: mcp-esptool-server:${VERSION:-latest}
    environment:
      - SERVER_ID=1
      - SERVER_PORT=8080
      - LOG_LEVEL=${LOG_LEVEL:-INFO}
      - DATABASE_URL=postgresql://esptool:${DB_PASSWORD}@postgres:5432/esptool
      - REDIS_URL=redis://redis:6379/0
      - ESP_DEVICE_BASE_PATH=/dev/serial
      - PRODUCTION_MODE=true
      - MAX_CONCURRENT_OPERATIONS=10
      - ENABLE_METRICS=true
      - METRICS_PORT=9090
    volumes:
      - ./data/server-1:/app/data
      - ./logs:/app/logs
      - ./config:/app/config:ro
      - /dev/serial:/dev/serial:rw
    networks:
      - backend
    restart: unless-stopped
    depends_on:
      - postgres
      - redis
    labels:
      - "com.docker.compose.service=esptool-server"

  esptool-server-2:
    <<: *esptool-server
    environment:
      - SERVER_ID=2
      - SERVER_PORT=8080
      - LOG_LEVEL=${LOG_LEVEL:-INFO}
      - DATABASE_URL=postgresql://esptool:${DB_PASSWORD}@postgres:5432/esptool
      - REDIS_URL=redis://redis:6379/0
      - ESP_DEVICE_BASE_PATH=/dev/serial
      - PRODUCTION_MODE=true
    volumes:
      - ./data/server-2:/app/data
      - ./logs:/app/logs
      - ./config:/app/config:ro
      - /dev/serial:/dev/serial:rw

  esptool-server-3:
    <<: *esptool-server
    environment:
      - SERVER_ID=3
      - SERVER_PORT=8080
      - LOG_LEVEL=${LOG_LEVEL:-INFO}
      - DATABASE_URL=postgresql://esptool:${DB_PASSWORD}@postgres:5432/esptool
      - REDIS_URL=redis://redis:6379/0
      - ESP_DEVICE_BASE_PATH=/dev/serial
      - PRODUCTION_MODE=true
    volumes:
      - ./data/server-3:/app/data
      - ./logs:/app/logs
      - ./config:/app/config:ro
      - /dev/serial:/dev/serial:rw

  # Database
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: esptool
      POSTGRES_USER: esptool
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
    networks:
      - backend
    restart: unless-stopped

  # Cache and session store
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 1gb --maxmemory-policy allkeys-lru
    volumes:
      - redis-data:/data
    networks:
      - backend
    restart: unless-stopped

  # Monitoring and metrics
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./config/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    networks:
      - backend
      - monitoring
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
    volumes:
      - grafana-data:/var/lib/grafana
      - ./config/grafana:/etc/grafana/provisioning:ro
    networks:
      - monitoring
    restart: unless-stopped
    depends_on:
      - prometheus

  # Log aggregation
  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"
    volumes:
      - loki-data:/tmp/loki
      - ./config/loki.yml:/etc/loki/local-config.yaml:ro
    networks:
      - monitoring
    restart: unless-stopped

volumes:
  postgres-data:
  redis-data:
  prometheus-data:
  grafana-data:
  loki-data:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
  monitoring:
    driver: bridge

⚙️ Configuration Management

Production Configuration

# src/mcp_esptool_server/config/production.py
import os
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from pathlib import Path

@dataclass
class ProductionConfig:
    """Production environment configuration"""

    # Server settings
    server_id: str = field(default_factory=lambda: os.getenv('SERVER_ID', '1'))
    server_port: int = int(os.getenv('SERVER_PORT', '8080'))
    max_workers: int = int(os.getenv('MAX_WORKERS', '4'))
    max_concurrent_operations: int = int(os.getenv('MAX_CONCURRENT_OPERATIONS', '10'))

    # Database configuration
    database_url: str = os.getenv('DATABASE_URL', 'postgresql://localhost:5432/esptool')
    redis_url: str = os.getenv('REDIS_URL', 'redis://localhost:6379/0')

    # Security settings
    enable_auth: bool = os.getenv('ENABLE_AUTH', 'true').lower() == 'true'
    jwt_secret: str = os.getenv('JWT_SECRET', 'change-me-in-production')
    api_key_required: bool = os.getenv('API_KEY_REQUIRED', 'true').lower() == 'true'

    # ESP device settings
    esp_device_base_path: str = os.getenv('ESP_DEVICE_BASE_PATH', '/dev/serial')
    max_device_connections: int = int(os.getenv('MAX_DEVICE_CONNECTIONS', '20'))
    device_timeout: int = int(os.getenv('DEVICE_TIMEOUT', '30'))

    # Logging and monitoring
    log_level: str = os.getenv('LOG_LEVEL', 'INFO')
    enable_metrics: bool = os.getenv('ENABLE_METRICS', 'true').lower() == 'true'
    metrics_port: int = int(os.getenv('METRICS_PORT', '9090'))

    # Performance tuning
    enable_caching: bool = os.getenv('ENABLE_CACHING', 'true').lower() == 'true'
    cache_ttl: int = int(os.getenv('CACHE_TTL', '300'))

    # Factory programming settings
    enable_factory_mode: bool = os.getenv('ENABLE_FACTORY_MODE', 'false').lower() == 'true'
    factory_batch_size: int = int(os.getenv('FACTORY_BATCH_SIZE', '100'))

    # Data retention
    log_retention_days: int = int(os.getenv('LOG_RETENTION_DAYS', '30'))
    metrics_retention_days: int = int(os.getenv('METRICS_RETENTION_DAYS', '90'))

    def __post_init__(self):
        """Validate configuration after initialization"""
        if self.enable_auth and self.jwt_secret == 'change-me-in-production':
            raise ValueError("JWT_SECRET must be set in production with authentication enabled")

        if not Path(self.esp_device_base_path).exists():
            raise ValueError(f"ESP device base path does not exist: {self.esp_device_base_path}")

Environment Configuration

# .env.production
# Core server settings
SERVER_ID=1
SERVER_PORT=8080
MAX_WORKERS=4
MAX_CONCURRENT_OPERATIONS=10

# Database and cache
DATABASE_URL=postgresql://esptool:secure_password@postgres:5432/esptool
REDIS_URL=redis://redis:6379/0

# Security (CHANGE THESE IN PRODUCTION)
ENABLE_AUTH=true
JWT_SECRET=your-256-bit-secret-key-here
API_KEY_REQUIRED=true

# ESP device configuration
ESP_DEVICE_BASE_PATH=/dev/serial
MAX_DEVICE_CONNECTIONS=20
DEVICE_TIMEOUT=30

# Logging and monitoring
LOG_LEVEL=INFO
ENABLE_METRICS=true
METRICS_PORT=9090

# Performance
ENABLE_CACHING=true
CACHE_TTL=300

# Factory programming
ENABLE_FACTORY_MODE=true
FACTORY_BATCH_SIZE=50

# Data retention
LOG_RETENTION_DAYS=30
METRICS_RETENTION_DAYS=90

# External services
GRAFANA_PASSWORD=secure_grafana_password
DB_PASSWORD=secure_db_password

🔐 Security Configuration

Authentication and Authorization

# src/mcp_esptool_server/security/auth.py
import jwt
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from fastapi import HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

class ProductionAuth:
    """Production authentication and authorization"""

    def __init__(self, config: ProductionConfig):
        self.config = config
        self.security = HTTPBearer()
        self.valid_api_keys: set = self._load_api_keys()

    def _load_api_keys(self) -> set:
        """Load valid API keys from configuration"""
        # In production, load from secure key management system
        api_keys_file = Path(self.config.config_dir) / "api_keys.txt"
        if api_keys_file.exists():
            return set(api_keys_file.read_text().strip().split('\n'))
        return set()

    def create_access_token(self, data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
        """Create JWT access token"""
        to_encode = data.copy()
        if expires_delta:
            expire = datetime.utcnow() + expires_delta
        else:
            expire = datetime.utcnow() + timedelta(minutes=15)

        to_encode.update({"exp": expire})
        encoded_jwt = jwt.encode(to_encode, self.config.jwt_secret, algorithm="HS256")
        return encoded_jwt

    def verify_token(self, credentials: HTTPAuthorizationCredentials) -> Dict[str, Any]:
        """Verify JWT token"""
        try:
            payload = jwt.decode(
                credentials.credentials,
                self.config.jwt_secret,
                algorithms=["HS256"]
            )
            return payload
        except jwt.PyJWTError:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Could not validate credentials"
            )

    def verify_api_key(self, api_key: str) -> bool:
        """Verify API key"""
        return api_key in self.valid_api_keys

# Security middleware
class SecurityMiddleware:
    """Production security middleware"""

    def __init__(self, app: FastMCP, auth: ProductionAuth):
        self.app = app
        self.auth = auth
        self._setup_security_headers()

    def _setup_security_headers(self):
        """Configure security headers"""
        @self.app.middleware("http")
        async def add_security_headers(request, call_next):
            response = await call_next(request)

            # Security headers
            response.headers["X-Content-Type-Options"] = "nosniff"
            response.headers["X-Frame-Options"] = "DENY"
            response.headers["X-XSS-Protection"] = "1; mode=block"
            response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
            response.headers["Content-Security-Policy"] = "default-src 'self'"

            return response

📊 Monitoring and Observability

Metrics Collection

# src/mcp_esptool_server/monitoring/metrics.py
from prometheus_client import Counter, Histogram, Gauge, start_http_server
from typing import Dict, Any
import time

class ProductionMetrics:
    """Production metrics collection"""

    def __init__(self, config: ProductionConfig):
        self.config = config

        # Operation metrics
        self.operations_total = Counter(
            'esptool_operations_total',
            'Total number of ESPTool operations',
            ['operation_type', 'status', 'server_id']
        )

        self.operation_duration = Histogram(
            'esptool_operation_duration_seconds',
            'Duration of ESPTool operations',
            ['operation_type', 'server_id']
        )

        # Device metrics
        self.connected_devices = Gauge(
            'esptool_connected_devices',
            'Number of connected ESP devices',
            ['server_id']
        )

        self.device_operations = Counter(
            'esptool_device_operations_total',
            'Total device operations by port',
            ['port', 'operation', 'status', 'server_id']
        )

        # System metrics
        self.active_connections = Gauge(
            'esptool_active_connections',
            'Number of active MCP connections',
            ['server_id']
        )

        self.memory_usage = Gauge(
            'esptool_memory_usage_bytes',
            'Memory usage in bytes',
            ['server_id']
        )

        # Error metrics
        self.errors_total = Counter(
            'esptool_errors_total',
            'Total number of errors',
            ['error_type', 'component', 'server_id']
        )

        if config.enable_metrics:
            start_http_server(config.metrics_port)

    def record_operation(self, operation_type: str, duration: float, status: str):
        """Record operation metrics"""
        self.operations_total.labels(
            operation_type=operation_type,
            status=status,
            server_id=self.config.server_id
        ).inc()

        self.operation_duration.labels(
            operation_type=operation_type,
            server_id=self.config.server_id
        ).observe(duration)

    def record_device_operation(self, port: str, operation: str, status: str):
        """Record device-specific operation"""
        self.device_operations.labels(
            port=port,
            operation=operation,
            status=status,
            server_id=self.config.server_id
        ).inc()

    def update_connected_devices(self, count: int):
        """Update connected devices count"""
        self.connected_devices.labels(server_id=self.config.server_id).set(count)

    def record_error(self, error_type: str, component: str):
        """Record error occurrence"""
        self.errors_total.labels(
            error_type=error_type,
            component=component,
            server_id=self.config.server_id
        ).inc()

Logging Configuration

# src/mcp_esptool_server/logging/production.py
import logging
import logging.config
from pathlib import Path
import json

LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "json": {
            "()": "pythonjsonlogger.jsonlogger.JsonFormatter",
            "format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d"
        },
        "standard": {
            "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
        }
    },
    "handlers": {
        "console": {
            "level": "INFO",
            "class": "logging.StreamHandler",
            "formatter": "standard",
            "stream": "ext://sys.stdout"
        },
        "file": {
            "level": "DEBUG",
            "class": "logging.handlers.RotatingFileHandler",
            "formatter": "json",
            "filename": "/app/logs/esptool-server.log",
            "maxBytes": 10485760,  # 10MB
            "backupCount": 5
        },
        "error_file": {
            "level": "ERROR",
            "class": "logging.handlers.RotatingFileHandler",
            "formatter": "json",
            "filename": "/app/logs/esptool-errors.log",
            "maxBytes": 10485760,
            "backupCount": 5
        }
    },
    "loggers": {
        "mcp_esptool_server": {
            "level": "DEBUG",
            "handlers": ["console", "file", "error_file"],
            "propagate": False
        },
        "esptool": {
            "level": "INFO",
            "handlers": ["file"],
            "propagate": False
        }
    },
    "root": {
        "level": "INFO",
        "handlers": ["console"]
    }
}

def setup_production_logging(config: ProductionConfig):
    """Set up production logging configuration"""

    # Ensure log directory exists
    log_dir = Path("/app/logs")
    log_dir.mkdir(exist_ok=True)

    # Update logging level from configuration
    LOGGING_CONFIG["handlers"]["console"]["level"] = config.log_level
    LOGGING_CONFIG["loggers"]["mcp_esptool_server"]["level"] = config.log_level

    # Configure logging
    logging.config.dictConfig(LOGGING_CONFIG)

    # Set up structured logging context
    logger = logging.getLogger("mcp_esptool_server")
    logger.info("Production logging initialized", extra={
        "server_id": config.server_id,
        "log_level": config.log_level,
        "environment": "production"
    })

🚀 Deployment Automation

Kubernetes Deployment

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-esptool-server
  labels:
    app: mcp-esptool-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mcp-esptool-server
  template:
    metadata:
      labels:
        app: mcp-esptool-server
    spec:
      containers:
      - name: mcp-esptool-server
        image: mcp-esptool-server:latest
        ports:
        - containerPort: 8080
        - containerPort: 9090  # Metrics
        env:
        - name: SERVER_ID
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: esptool-secrets
              key: database-url
        - name: REDIS_URL
          valueFrom:
            secretKeyRef:
              name: esptool-secrets
              key: redis-url
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: esptool-secrets
              key: jwt-secret
        volumeMounts:
        - name: device-access
          mountPath: /dev/serial
        - name: config
          mountPath: /app/config
          readOnly: true
        - name: data
          mountPath: /app/data
        resources:
          requests:
            memory: "512Mi"
            cpu: "250m"
          limits:
            memory: "1Gi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
      volumes:
      - name: device-access
        hostPath:
          path: /dev/serial
      - name: config
        configMap:
          name: esptool-config
      - name: data
        persistentVolumeClaim:
          claimName: esptool-data

---
apiVersion: v1
kind: Service
metadata:
  name: mcp-esptool-service
spec:
  selector:
    app: mcp-esptool-server
  ports:
  - name: http
    port: 80
    targetPort: 8080
  - name: metrics
    port: 9090
    targetPort: 9090
  type: ClusterIP

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mcp-esptool-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
  - hosts:
    - esptool.yourdomain.com
    secretName: esptool-tls
  rules:
  - host: esptool.yourdomain.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: mcp-esptool-service
            port:
              number: 80

CI/CD Pipeline

# .github/workflows/production-deploy.yml
name: Production Deployment

on:
  push:
    tags:
      - 'v*'

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

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

    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.11'

    - name: Install dependencies
      run: |
        pip install uv
        uv venv
        uv pip install -e ".[dev,testing]"

    - name: Run tests
      run: |
        uv run pytest tests/ --cov=src --cov-fail-under=85

    - name: Security scan
      run: |
        uv run bandit -r src/
        uv run safety check

  build:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
    - uses: actions/checkout@v4

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=tag
          type=raw,value=latest

    - name: Build and push
      uses: docker/build-push-action@v5
      with:
        context: .
        target: production
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
    - uses: actions/checkout@v4

    - name: Configure kubectl
      run: |
        echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
        export KUBECONFIG=kubeconfig

    - name: Deploy to production
      run: |
        kubectl set image deployment/mcp-esptool-server \
          mcp-esptool-server=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}

        kubectl rollout status deployment/mcp-esptool-server --timeout=300s

    - name: Verify deployment
      run: |
        kubectl get pods -l app=mcp-esptool-server
        kubectl logs -l app=mcp-esptool-server --tail=50

📋 Production Checklist

Pre-Deployment

  • Security Configuration

    • Change default passwords and secrets
    • Configure JWT secrets
    • Set up API key management
    • Configure TLS certificates
    • Review firewall rules
  • Infrastructure Setup

    • Database cluster configured
    • Redis cache configured
    • Load balancer configured
    • Monitoring stack deployed
    • Log aggregation configured
  • Hardware Access

    • ESP device permissions configured
    • USB device access verified
    • Device path mapping documented
    • Hardware redundancy planned

Post-Deployment

  • Monitoring Setup

    • Grafana dashboards configured
    • Alerting rules configured
    • Log analysis configured
    • Performance baselines established
  • Operational Procedures

    • Backup procedures documented
    • Disaster recovery tested
    • Scaling procedures documented
    • Maintenance windows scheduled
  • Team Training

    • Operations team trained
    • Troubleshooting guides created
    • Escalation procedures documented
    • User documentation published

This production deployment guide ensures enterprise-grade reliability, security, and scalability for the FastMCP ESPTool server in production environments.

★ Insight ───────────────────────────────────── Enterprise-Grade Deployment: This production guide demonstrates how to scale the FastMCP ESPTool server from development to enterprise production environments with proper security, monitoring, and operational procedures.

Hardware Integration Challenges: The unique aspect of this deployment is managing physical ESP device access in containerized environments, requiring careful consideration of USB device mapping and permissions across different deployment platforms.

Observability Excellence: The comprehensive monitoring, logging, and metrics collection ensures production visibility and enables proactive problem resolution, critical for embedded development workflows where hardware issues can be difficult to diagnose. ─────────────────────────────────────────────────

<function_calls> [{"content": "Create project initialization and setup documentation", "status": "completed", "activeForm": "Creating project initialization and setup documentation"}, {"content": "Design concrete implementation examples", "status": "completed", "activeForm": "Designing concrete implementation examples"}, {"content": "Create production deployment guide", "status": "completed", "activeForm": "Creating production deployment guide"}]