Deployment: Ab in die Production

Der Server läuft lokal? Schön. Aber jetzt soll er 24/7 laufen, automatisch starten und sich selbst überwachen. Ich zeige Dir, wie ich das seit Jahren mache. Ohne Docker-Drama, ohne Kubernetes-Komplexität. Einfach solide.

Systemd Service einrichten

Linux macht's uns einfach. Ein Systemd-Service und der Server läuft wie von selbst:

# /etc/systemd/system/mcp-content-metadata.service
[Unit]
Description=MCP Content Metadata Server
After=network.target mariadb.service
Wants=mariadb.service

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/mcp/content-metadata-server

# Environment
Environment="PYTHONPATH=/var/www/mcp/content-metadata-server"
Environment="DATENBANK_CONFIG_DIR=/var/www/mcp/content-metadata-server/config"

# Start-Befehl
ExecStart=/var/www/mcp/content-metadata-server/venv/bin/python /var/www/mcp/content-metadata-server/server.py

# Restart-Policy
Restart=always
RestartSec=10

# Logging
StandardOutput=append:/var/log/mcp/content-metadata.log
StandardError=append:/var/log/mcp/content-metadata-error.log

# Security (optional aber empfohlen)
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/mcp/content-metadata-server/logs

[Install]
WantedBy=multi-user.target

Service aktivieren und starten:

# Service-Datei neu laden
sudo systemctl daemon-reload

# Service aktivieren (startet beim Boot)
sudo systemctl enable mcp-content-metadata

# Service starten
sudo systemctl start mcp-content-metadata

# Status prüfen
sudo systemctl status mcp-content-metadata

# Logs anschauen
sudo journalctl -u mcp-content-metadata -f

Der Service startet jetzt automatisch beim Boot und wird bei Abstürzen neu gestartet. Das ist Production-ready.

Production-Config

Production ist nicht Development. Andere Configs, andere Regeln:

# config/production.json
{
  "environment": "production",

  "database": {
    "connection_pool": {
      "min_size": 2,
      "max_size": 10,
      "timeout": 30,
      "idle_time": 3600,
      "retry_attempts": 3
    }
  },

  "security": {
    "rate_limiting": {
      "queries_per_minute": 100,
      "burst_size": 20,
      "block_duration": 300
    },
    "allowed_ips": [
      "127.0.0.1",
      "::1",
      "10.0.0.0/8"
    ],
    "require_https": true,
    "cors_origins": [
      "https://karlkratz.de",
      "/"
    ]
  },

  "logging": {
    "level": "WARNING",
    "rotation": {
      "when": "midnight",
      "interval": 1,
      "backup_count": 30
    },
    "sentry": {
      "enabled": true,
      "dsn": "YOUR_SENTRY_DSN",
      "environment": "production"
    }
  },

  "monitoring": {
    "health_check_port": 8080,
    "metrics_enabled": true,
    "prometheus_port": 9090
  }
}

Health Checks einbauen

Wie merke ich, ob der Server noch lebt? Health Checks!

# health_check.py
from fastapi import FastAPI
from datetime import datetime
import psutil
import pymysql

app = FastAPI()

@app.get("/health")
async def health_check():
    """Basis Health Check"""
    return {
        "status": "healthy",
        "timestamp": datetime.now().isoformat()
    }

@app.get("/health/detailed")
async def detailed_health():
    """Detaillierter Health Check"""

    health = {
        "status": "healthy",
        "timestamp": datetime.now().isoformat(),
        "checks": {}
    }

    # CPU Check
    cpu_percent = psutil.cpu_percent(interval=1)
    health["checks"]["cpu"] = {
        "status": "ok" if cpu_percent < 80 else "warning",
        "value": f"{cpu_percent}%"
    }

    # Memory Check
    memory = psutil.virtual_memory()
    health["checks"]["memory"] = {
        "status": "ok" if memory.percent < 80 else "warning",
        "value": f"{memory.percent}%"
    }

    # Database Check
    try:
        conn = pymysql.connect(
            host='localhost',
            user='content_reader',
            password='xxx',
            database='karlkratz_de'
        )
        conn.ping()
        conn.close()
        health["checks"]["database"] = {
            "status": "ok",
            "response_time": "< 100ms"
        }
    except Exception as e:
        health["checks"]["database"] = {
            "status": "error",
            "error": str(e)
        }
        health["status"] = "unhealthy"

    # Disk Check
    disk = psutil.disk_usage('/')
    health["checks"]["disk"] = {
        "status": "ok" if disk.percent < 80 else "warning",
        "value": f"{disk.percent}%"
    }

    return health

Monitoring mit Prometheus

Metriken sammeln, bevor es knallt:

# metrics.py
from prometheus_client import Counter, Histogram, Gauge
import time

# Metriken definieren
query_counter = Counter(
    'mcp_queries_total',
    'Total number of queries',
    ['operation', 'status']
)

query_duration = Histogram(
    'mcp_query_duration_seconds',
    'Query duration in seconds',
    ['operation']
)

active_connections = Gauge(
    'mcp_active_connections',
    'Number of active database connections'
)

# In Deinem Code verwenden
class MetricsMiddleware:
    def track_query(self, operation, func):
        """Decorator für Query-Tracking"""
        def wrapper(*args, **kwargs):
            start = time.time()

            try:
                result = func(*args, **kwargs)
                query_counter.labels(
                    operation=operation,
                    status='success'
                ).inc()
                return result

            except Exception as e:
                query_counter.labels(
                    operation=operation,
                    status='error'
                ).inc()
                raise

            finally:
                duration = time.time() - start
                query_duration.labels(
                    operation=operation
                ).observe(duration)

        return wrapper

Mit Prometheus + Grafana siehst Du genau, was in Deinem Server passiert. Bevor die Kunden anrufen.

Backup-Strategie

Backups sind wie Versicherungen: Du brauchst sie hoffentlich nie, aber wenn doch...

#!/bin/bash
# backup_mcp.sh

# Variablen
BACKUP_DIR="/backup/mcp"
DATE=$(date +%Y%m%d_%H%M%S)
MCP_DIR="/var/www/mcp/content-metadata-server"

# Backup-Verzeichnis erstellen
mkdir -p "$BACKUP_DIR/$DATE"

# 1. Code Backup
echo "Backing up code..."
tar -czf "$BACKUP_DIR/$DATE/code.tar.gz" \
    --exclude="$MCP_DIR/venv" \
    --exclude="$MCP_DIR/logs" \
    --exclude="$MCP_DIR/__pycache__" \
    "$MCP_DIR"

# 2. Config Backup (verschlüsselt!)
echo "Backing up configs (encrypted)..."
tar -czf - "$MCP_DIR/config" | \
    openssl enc -aes-256-cbc -salt -pass pass:YOUR_SECRET \
    -out "$BACKUP_DIR/$DATE/config.tar.gz.enc"

# 3. Database Schema Backup
echo "Backing up database schema..."
mysqldump -u root -p \
    --no-data \
    --routines \
    --triggers \
    karlkratz_de content_metadata \
    > "$BACKUP_DIR/$DATE/schema.sql"

# 4. Logs der letzten 7 Tage
echo "Backing up recent logs..."
find "$MCP_DIR/logs" -type f -mtime -7 \
    -exec cp {} "$BACKUP_DIR/$DATE/" \;

# 5. Alte Backups löschen (älter als 30 Tage)
echo "Cleaning old backups..."
find "$BACKUP_DIR" -type d -mtime +30 -exec rm -rf {} \;

echo "[OK] Backup complete: $BACKUP_DIR/$DATE"

# Optional: Zu Remote-Server kopieren
# rsync -avz "$BACKUP_DIR/$DATE" backup@remote-server:/backups/mcp/

Cron-Job für automatische Backups:

# Täglich um 3 Uhr nachts
0 3 * * * /usr/local/bin/backup_mcp.sh >> /var/log/mcp_backup.log 2>&1

Reverse Proxy als Schutzschild

Es gibt Situationen, da möchtest Du Deinen MCP Server nicht direkt aus dem Internet erreichbar machen. Vielleicht läuft er auf einem internen Port, vielleicht willst Du zusätzliche Sicherheitsebenen, oder Du brauchst SSL-Terminierung an einer zentralen Stelle. Ein Reverse Proxy wie Apache oder Nginx kann hier als intelligente Zwischenschicht fungieren.

Was bringt Dir ein Reverse Proxy? Zunächst einmal Sicherheit durch Abstraktion. Angreifer sehen nur den Proxy, nicht Deinen eigentlichen Server. Du kannst Rate Limiting zentral steuern, SSL-Zertifikate an einer Stelle verwalten und hast einen Single Point für Security Headers. Außerdem kannst Du mehrere Backend-Server loadbalancen, wenn Dein System wächst. Der Proxy kann statische Assets cachen und entlastet damit Deinen MCP Server. Und wenn Du mal wartungsarbeiten machst, zeigt der Proxy eine freundliche Wartungsseite statt eines Verbindungsfehlers.

Hier ein praxiserprobtes Nginx-Setup als Beispiel:

# /etc/nginx/sites-available/mcp-server
upstream mcp_backend {
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name mcp.karlkratz.de;

    # SSL Certificates
    ssl_certificate /etc/letsencrypt/live/mcp.karlkratz.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mcp.karlkratz.de/privkey.pem;

    # Security Headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Rate Limiting
    limit_req_zone $binary_remote_addr zone=mcp:10m rate=10r/s;
    limit_req zone=mcp burst=20 nodelay;

    # Health Check Endpoint (öffentlich)
    location /health {
        proxy_pass http://mcp_backend/health;
        access_log off;
    }

    # MCP API (authentifiziert)
    location /rpc {
        # IP-Whitelist
        allow 127.0.0.1;
        allow 10.0.0.0/8;
        deny all;

        # Proxy Settings
        proxy_pass http://mcp_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Metrics (intern)
    location /metrics {
        allow 127.0.0.1;
        deny all;
        proxy_pass http://127.0.0.1:9090/metrics;
    }
}

Environment-Management: Entwicklung, Staging und Production sauber trennen

Wenn Du professionell entwickelst, brauchst Du verschiedene Umgebungen für verschiedene Zwecke. Das klingt erstmal nach Overhead, aber glaub mir: Der erste Produktions-Crash, den Du durch Tests in Staging verhindert hast, macht den Aufwand mehr als wett.

Warum verschiedene Umgebungen? In Development experimentierst Du wild herum, probierst neue Features, machst auch mal was kaputt. Das ist völlig in Ordnung, dafür ist Development da. In Staging testest Du unter produktionsnahen Bedingungen, aber ohne echte Nutzer zu gefährden. Hier laufen Deine Integrationstests, hier prüfst Du Performance unter Last. Und Production? Das ist die heilige Umgebung, wo alles stabil laufen muss.

Was unterscheidet die Umgebungen konkret? Log-Level zum Beispiel. In Development willst Du jeden Debug-Output sehen, in Production nur Warnungen und Fehler. Datenbankverbindungen zeigen auf verschiedene Server. API-Keys und Credentials sind andere. Rate Limits sind in Development großzügiger. Error-Reporting geht in Production an Sentry, in Development auf die Konsole.

Mit diesem Python-Script managst Du verschiedene Environments elegant:

# deploy.py
#!/usr/bin/env python3
"""
Deployment-Helper für verschiedene Environments
"""

import os
import sys
import shutil
import argparse

ENVIRONMENTS = {
    'development': {
        'config_dir': 'config/dev',
        'log_level': 'DEBUG',
        'db_host': 'localhost'
    },
    'staging': {
        'config_dir': 'config/staging',
        'log_level': 'INFO',
        'db_host': 'staging-db.local'
    },
    'production': {
        'config_dir': 'config/prod',
        'log_level': 'WARNING',
        'db_host': 'prod-db.local'
    }
}

def deploy(environment):
    """Deploy für spezifisches Environment"""

    if environment not in ENVIRONMENTS:
        print(f"[FEHLER] Unknown environment: {environment}")
        sys.exit(1)

    env = ENVIRONMENTS[environment]

    print(f"Deploying to {environment}...")
    print("=" * 40)

    # 1. Config kopieren
    print("Copying configs...")
    shutil.copytree(
        env['config_dir'],
        'config',
        dirs_exist_ok=True
    )

    # 2. Environment-Variablen setzen
    print("Setting environment...")
    os.environ['MCP_ENV'] = environment
    os.environ['LOG_LEVEL'] = env['log_level']
    os.environ['DB_HOST'] = env['db_host']

    # 3. Dependencies prüfen
    print("Checking dependencies...")
    os.system('pip install -r requirements.txt')

    # 4. Tests laufen lassen
    print("Running tests...")
    result = os.system('python -m pytest tests/')
    if result != 0:
        print("[FEHLER] Tests failed! Aborting deployment.")
        sys.exit(1)

    # 5. Service neu starten
    print("Restarting service...")
    os.system(f'sudo systemctl restart mcp-content-metadata')

    print(f"[OK] Deployed to {environment}!")

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('environment', choices=ENVIRONMENTS.keys())
    args = parser.parse_args()

    deploy(args.environment)

Log-Rotation

Logs fressen Platz. Rotation ist Pflicht:

# /etc/logrotate.d/mcp-server
/var/log/mcp/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 644 www-data www-data
    sharedscripts
    postrotate
        systemctl reload mcp-content-metadata > /dev/null 2>&1 || true
    endscript
}

Security Hardening

Production heißt: Paranoid sein.

# security_hardening.sh
#!/bin/bash

echo "Hardening MCP Server..."
echo "========================"

# 1. Firewall Rules
echo "Setting up firewall..."
ufw allow from 127.0.0.1 to any port 3000
ufw allow from 10.0.0.0/8 to any port 3000
ufw deny 3000

# 2. File Permissions
echo "Setting permissions..."
chmod 700 /var/www/mcp/content-metadata-server
chmod 600 /var/www/mcp/content-metadata-server/config/*.json
chmod 755 /var/www/mcp/content-metadata-server/logs
chown -R www-data:www-data /var/www/mcp/content-metadata-server

# 3. SELinux Context (wenn aktiviert)
if command -v getenforce &> /dev/null; then
    echo "Setting SELinux context..."
    semanage fcontext -a -t httpd_sys_content_t "/var/www/mcp(/.*)?"
    restorecon -Rv /var/www/mcp
fi

# 4. Fail2ban für MCP
echo "Configuring fail2ban..."
cat > /etc/fail2ban/filter.d/mcp-server.conf << EOF
[Definition]
failregex = .*Rate limit exceeded.*from
            .*SQL injection attempt.*from
            .*Authentication failed.*from
ignoreregex =
EOF

cat > /etc/fail2ban/jail.d/mcp-server.conf << EOF
[mcp-server]
enabled = true
port = 3000
filter = mcp-server
logpath = /var/log/mcp/content-metadata.log
maxretry = 5
findtime = 600
bantime = 3600
EOF

systemctl restart fail2ban

echo "[OK] Hardening complete!"

Deployment-Checkliste

Vor dem Go-Live:

Post-Deployment Monitoring

Nach dem Deployment: Augen auf!

# monitor_deployment.sh
#!/bin/bash

echo "Monitoring new deployment..."
echo "============================"

# 1. Service Status
systemctl status mcp-content-metadata --no-pager

# 2. Recent Errors
echo -e "\nRecent errors:"
journalctl -u mcp-content-metadata -p err --since "1 hour ago" --no-pager

# 3. Performance Metrics
echo -e "\nPerformance:"
curl -s localhost:8080/health/detailed | jq .

# 4. Active Connections
echo -e "\nDatabase connections:"
mysql -e "SHOW PROCESSLIST;" | grep content_

# 5. Disk Usage
echo -e "\nDisk usage:"
df -h | grep -E "Filesystem|/var/www"

# 6. Memory Usage
echo -e "\nMemory usage:"
free -h

# Alert wenn was nicht stimmt
if systemctl is-active mcp-content-metadata | grep -q inactive; then
    echo "[ALERT] Service is not running!"
    # Hier könnte eine E-Mail oder Slack-Nachricht raus
fi

Was ich gelernt habe

Nach vielen Deployments:

Der beste Deployment ist der, bei dem keiner merkt, dass Du deployed hast.

Wenn Dein Server läuft, wird früher oder später etwas schiefgehen. Das ist normal. Im nächsten Kapitel teile ich Troubleshooting-Strategien, die sich über die Jahre angesammelt haben. Du lernst, wie Du systematisch Fehler findest, häufige Probleme löst und mit Emergency Fixes schnell reagierst, wenn's mal brennt. Weiter zu den Troubleshooting-Strategien