"Feature: Add environment variables example and monitoring script"
This commit is contained in:
parent
9f6219832c
commit
6751e4a54b
@ -54,15 +54,7 @@ def create_app(config_name=None):
|
|||||||
# Initialisiere Netzwerkkonfiguration
|
# Initialisiere Netzwerkkonfiguration
|
||||||
network_config = NetworkConfig(app)
|
network_config = NetworkConfig(app)
|
||||||
|
|
||||||
# Registriere Blueprint
|
# Globale Variablen setzen
|
||||||
app.register_blueprint(frontend_v2, url_prefix='/frontend_v2')
|
|
||||||
|
|
||||||
# Konfiguriere statische Dateien für Frontend v2
|
|
||||||
@app.route('/frontend_v2/static/<path:filename>')
|
|
||||||
def frontend_v2_static(filename):
|
|
||||||
return send_from_directory(os.path.join(app.root_path, 'frontend_v2/static'), filename)
|
|
||||||
|
|
||||||
# Globale Variablen
|
|
||||||
app.config['PRINTERS'] = json.loads(app.config.get('PRINTERS', '{}'))
|
app.config['PRINTERS'] = json.loads(app.config.get('PRINTERS', '{}'))
|
||||||
|
|
||||||
# Database functions registrieren
|
# Database functions registrieren
|
||||||
@ -80,6 +72,12 @@ def create_app(config_name=None):
|
|||||||
# Error-Handler registrieren
|
# Error-Handler registrieren
|
||||||
register_error_handlers(app)
|
register_error_handlers(app)
|
||||||
|
|
||||||
|
# Blueprint registrieren
|
||||||
|
register_blueprints(app)
|
||||||
|
|
||||||
|
# Middleware registrieren
|
||||||
|
register_middleware(app)
|
||||||
|
|
||||||
# Hintergrund-Tasks registrieren
|
# Hintergrund-Tasks registrieren
|
||||||
register_background_tasks(app)
|
register_background_tasks(app)
|
||||||
|
|
||||||
@ -1813,17 +1811,44 @@ def register_auth_functions(app):
|
|||||||
|
|
||||||
def register_api_routes(app):
|
def register_api_routes(app):
|
||||||
"""Registriert alle API-Routen."""
|
"""Registriert alle API-Routen."""
|
||||||
# API-Routen sind bereits global definiert
|
# API-Routen sind bereits global als Funktionen definiert
|
||||||
|
# Diese werden automatisch registriert, wenn sie mit @app.route dekoriert sind
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def register_web_routes(app):
|
def register_web_routes(app):
|
||||||
"""Registriert alle Web-UI-Routen."""
|
"""Registriert alle Web-UI-Routen."""
|
||||||
# Web-Routen sind bereits global definiert
|
# Web-Routen sind bereits global als Funktionen definiert
|
||||||
|
# Diese werden automatisch registriert, wenn sie mit @app.route dekoriert sind
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def register_error_handlers(app):
|
def register_error_handlers(app):
|
||||||
"""Registriert Error-Handler."""
|
"""Registriert Error-Handler."""
|
||||||
# Error-Handler sind bereits global definiert
|
# Error-Handler sind bereits global definiert
|
||||||
|
# Diese werden automatisch registriert, wenn sie mit @app.errorhandler dekoriert sind
|
||||||
|
pass
|
||||||
|
|
||||||
|
def register_blueprints(app):
|
||||||
|
"""Registriert alle Flask-Blueprints."""
|
||||||
|
# Frontend V2 Blueprint
|
||||||
|
app.register_blueprint(frontend_v2, url_prefix='/frontend_v2')
|
||||||
|
|
||||||
|
# Monitoring Blueprint
|
||||||
|
from monitoring import monitoring_bp
|
||||||
|
app.register_blueprint(monitoring_bp)
|
||||||
|
|
||||||
|
# Konfiguriere statische Dateien für Frontend v2
|
||||||
|
@app.route('/frontend_v2/static/<path:filename>')
|
||||||
|
def frontend_v2_static(filename):
|
||||||
|
return send_from_directory(os.path.join(app.root_path, 'frontend_v2/static'), filename)
|
||||||
|
|
||||||
|
def register_middleware(app):
|
||||||
|
"""Registriert Middleware-Komponenten."""
|
||||||
|
# Monitoring-Middleware
|
||||||
|
if app.config.get('FLASK_ENV') != 'testing':
|
||||||
|
from monitoring import request_metrics
|
||||||
|
request_metrics.init_app(app)
|
||||||
|
|
||||||
|
# Sicherheits-Middleware wird bereits in der Konfiguration registriert
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def register_background_tasks(app):
|
def register_background_tasks(app):
|
||||||
|
@ -33,6 +33,18 @@ class Config:
|
|||||||
# Drucker-Konfiguration
|
# Drucker-Konfiguration
|
||||||
PRINTERS = os.environ.get('PRINTERS', '{}')
|
PRINTERS = os.environ.get('PRINTERS', '{}')
|
||||||
|
|
||||||
|
# API-Konfiguration
|
||||||
|
API_KEY = os.environ.get('API_KEY')
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
RATE_LIMIT_ENABLED = True
|
||||||
|
MAX_REQUESTS_PER_MINUTE = int(os.environ.get('MAX_REQUESTS_PER_MINUTE', '100'))
|
||||||
|
RATE_LIMIT_WINDOW_MINUTES = int(os.environ.get('RATE_LIMIT_WINDOW_MINUTES', '15'))
|
||||||
|
|
||||||
|
# Security
|
||||||
|
SECURITY_ENABLED = True
|
||||||
|
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init_app(app):
|
def init_app(app):
|
||||||
"""Initialisierung der Anwendung mit der Konfiguration."""
|
"""Initialisierung der Anwendung mit der Konfiguration."""
|
||||||
@ -50,6 +62,10 @@ class DevelopmentConfig(Config):
|
|||||||
# Kürzere Job-Check-Intervalle für schnellere Entwicklung
|
# Kürzere Job-Check-Intervalle für schnellere Entwicklung
|
||||||
JOB_CHECK_INTERVAL = int(os.environ.get('JOB_CHECK_INTERVAL', '30'))
|
JOB_CHECK_INTERVAL = int(os.environ.get('JOB_CHECK_INTERVAL', '30'))
|
||||||
|
|
||||||
|
# Weniger strikte Sicherheit in Development
|
||||||
|
SECURITY_ENABLED = False
|
||||||
|
RATE_LIMIT_ENABLED = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init_app(app):
|
def init_app(app):
|
||||||
Config.init_app(app)
|
Config.init_app(app)
|
||||||
@ -76,6 +92,14 @@ class ProductionConfig(Config):
|
|||||||
# Längere Job-Check-Intervalle für bessere Performance
|
# Längere Job-Check-Intervalle für bessere Performance
|
||||||
JOB_CHECK_INTERVAL = int(os.environ.get('JOB_CHECK_INTERVAL', '60'))
|
JOB_CHECK_INTERVAL = int(os.environ.get('JOB_CHECK_INTERVAL', '60'))
|
||||||
|
|
||||||
|
# Produktions-Sicherheit
|
||||||
|
SECURITY_ENABLED = True
|
||||||
|
RATE_LIMIT_ENABLED = True
|
||||||
|
MAX_REQUESTS_PER_MINUTE = int(os.environ.get('MAX_REQUESTS_PER_MINUTE', '60'))
|
||||||
|
|
||||||
|
# HTTPS-Enforcement (wenn verfügbar)
|
||||||
|
FORCE_HTTPS = os.environ.get('FORCE_HTTPS', 'False').lower() == 'true'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init_app(app):
|
def init_app(app):
|
||||||
Config.init_app(app)
|
Config.init_app(app)
|
||||||
@ -111,8 +135,29 @@ class ProductionConfig(Config):
|
|||||||
error_handler.setLevel(logging.ERROR)
|
error_handler.setLevel(logging.ERROR)
|
||||||
app.logger.addHandler(error_handler)
|
app.logger.addHandler(error_handler)
|
||||||
|
|
||||||
|
# Security-Logging
|
||||||
|
security_handler = RotatingFileHandler(
|
||||||
|
'logs/security.log',
|
||||||
|
maxBytes=Config.LOG_MAX_BYTES,
|
||||||
|
backupCount=Config.LOG_BACKUP_COUNT
|
||||||
|
)
|
||||||
|
security_handler.setFormatter(logging.Formatter(
|
||||||
|
'%(asctime)s SECURITY %(levelname)s: %(message)s [%(name)s]'
|
||||||
|
))
|
||||||
|
security_handler.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Security-Logger
|
||||||
|
security_logger = logging.getLogger('security')
|
||||||
|
security_logger.addHandler(security_handler)
|
||||||
|
security_logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
app.logger.setLevel(logging.INFO)
|
app.logger.setLevel(logging.INFO)
|
||||||
app.logger.info('MYP Backend starting in production mode')
|
app.logger.info('MYP Backend starting in production mode')
|
||||||
|
|
||||||
|
# Sicherheits-Middleware registrieren
|
||||||
|
if app.config.get('SECURITY_ENABLED', True):
|
||||||
|
from security import security_middleware
|
||||||
|
security_middleware.init_app(app)
|
||||||
|
|
||||||
class TestingConfig(Config):
|
class TestingConfig(Config):
|
||||||
"""Konfiguration für die Testumgebung."""
|
"""Konfiguration für die Testumgebung."""
|
||||||
@ -132,6 +177,10 @@ class TestingConfig(Config):
|
|||||||
# Kürzere Job-Check-Intervalle für Tests
|
# Kürzere Job-Check-Intervalle für Tests
|
||||||
JOB_CHECK_INTERVAL = 5
|
JOB_CHECK_INTERVAL = 5
|
||||||
|
|
||||||
|
# Deaktiviere Sicherheit für Tests
|
||||||
|
SECURITY_ENABLED = False
|
||||||
|
RATE_LIMIT_ENABLED = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init_app(app):
|
def init_app(app):
|
||||||
Config.init_app(app)
|
Config.init_app(app)
|
||||||
|
1
backend/env.example
Normal file
1
backend/env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
330
backend/monitoring.py
Normal file
330
backend/monitoring.py
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
"""
|
||||||
|
Monitoring und Health Check Module für die MYP Flask-Anwendung.
|
||||||
|
Bietet Endpunkte für Systemüberwachung und Performance-Metriken.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, current_app
|
||||||
|
import psutil
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import datetime
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# Blueprint für Monitoring-Endpunkte
|
||||||
|
monitoring_bp = Blueprint('monitoring', __name__, url_prefix='/monitoring')
|
||||||
|
|
||||||
|
# Metriken-Speicher
|
||||||
|
metrics = {
|
||||||
|
'requests_total': defaultdict(int),
|
||||||
|
'request_duration': defaultdict(list),
|
||||||
|
'database_queries': 0,
|
||||||
|
'active_jobs': 0,
|
||||||
|
'error_count': defaultdict(int),
|
||||||
|
'startup_time': datetime.datetime.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
class HealthCheck:
|
||||||
|
"""Klasse für System-Health-Checks."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_database():
|
||||||
|
"""
|
||||||
|
Überprüft die Datenbankverbindung.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Status und Details der Datenbankverbindung
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
db_path = current_app.config.get('DATABASE', 'instance/myp.db')
|
||||||
|
|
||||||
|
# Bei In-Memory-DB für Tests
|
||||||
|
if db_path == ':memory:':
|
||||||
|
return {'status': 'healthy', 'message': 'In-Memory-Datenbank aktiv'}
|
||||||
|
|
||||||
|
# Datei-basierte Datenbank prüfen
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
return {'status': 'unhealthy', 'message': 'Datenbankdatei nicht gefunden'}
|
||||||
|
|
||||||
|
# Verbindung testen
|
||||||
|
conn = sqlite3.connect(db_path, timeout=5)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT 1')
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Dateigröße ermitteln
|
||||||
|
db_size = os.path.getsize(db_path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': 'healthy',
|
||||||
|
'message': 'Datenbankverbindung erfolgreich',
|
||||||
|
'database_path': db_path,
|
||||||
|
'database_size_bytes': db_size
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'status': 'unhealthy',
|
||||||
|
'message': f'Datenbankfehler: {str(e)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_disk_space():
|
||||||
|
"""
|
||||||
|
Überprüft den verfügbaren Festplattenspeicher.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Status und Details des Festplattenspeichers
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
disk_usage = psutil.disk_usage('.')
|
||||||
|
free_gb = disk_usage.free / (1024**3)
|
||||||
|
total_gb = disk_usage.total / (1024**3)
|
||||||
|
used_percent = (disk_usage.used / disk_usage.total) * 100
|
||||||
|
|
||||||
|
status = 'healthy'
|
||||||
|
if used_percent > 90:
|
||||||
|
status = 'critical'
|
||||||
|
elif used_percent > 80:
|
||||||
|
status = 'warning'
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': status,
|
||||||
|
'free_gb': round(free_gb, 2),
|
||||||
|
'total_gb': round(total_gb, 2),
|
||||||
|
'used_percent': round(used_percent, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'status': 'unhealthy',
|
||||||
|
'message': f'Festplattenfehler: {str(e)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_memory():
|
||||||
|
"""
|
||||||
|
Überprüft die Speichernutzung.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Status und Details der Speichernutzung
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
|
||||||
|
status = 'healthy'
|
||||||
|
if memory.percent > 90:
|
||||||
|
status = 'critical'
|
||||||
|
elif memory.percent > 80:
|
||||||
|
status = 'warning'
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': status,
|
||||||
|
'total_gb': round(memory.total / (1024**3), 2),
|
||||||
|
'available_gb': round(memory.available / (1024**3), 2),
|
||||||
|
'used_percent': round(memory.percent, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'status': 'unhealthy',
|
||||||
|
'message': f'Speicherfehler: {str(e)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_background_threads():
|
||||||
|
"""
|
||||||
|
Überprüft die Hintergrund-Threads.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Status der Hintergrund-Threads
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
active_threads = [t.name for t in threading.enumerate() if t.is_alive()]
|
||||||
|
job_checker_running = any('job_checker' in name for name in active_threads)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': 'healthy' if job_checker_running else 'warning',
|
||||||
|
'job_checker_running': job_checker_running,
|
||||||
|
'active_threads': active_threads,
|
||||||
|
'thread_count': len(active_threads)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'status': 'unhealthy',
|
||||||
|
'message': f'Thread-Fehler: {str(e)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
@monitoring_bp.route('/health')
|
||||||
|
def health_check():
|
||||||
|
"""
|
||||||
|
Umfassender Health Check aller Systemkomponenten.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON: Status aller Systemkomponenten
|
||||||
|
"""
|
||||||
|
checks = {
|
||||||
|
'database': HealthCheck.check_database(),
|
||||||
|
'disk_space': HealthCheck.check_disk_space(),
|
||||||
|
'memory': HealthCheck.check_memory(),
|
||||||
|
'background_threads': HealthCheck.check_background_threads()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gesamtstatus bestimmen
|
||||||
|
overall_status = 'healthy'
|
||||||
|
for check in checks.values():
|
||||||
|
if check['status'] == 'unhealthy':
|
||||||
|
overall_status = 'unhealthy'
|
||||||
|
break
|
||||||
|
elif check['status'] in ['warning', 'critical']:
|
||||||
|
overall_status = 'degraded'
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'status': overall_status,
|
||||||
|
'timestamp': datetime.datetime.now().isoformat(),
|
||||||
|
'checks': checks
|
||||||
|
}
|
||||||
|
|
||||||
|
status_code = 200 if overall_status == 'healthy' else 503
|
||||||
|
return jsonify(response), status_code
|
||||||
|
|
||||||
|
@monitoring_bp.route('/health/simple')
|
||||||
|
def simple_health_check():
|
||||||
|
"""
|
||||||
|
Einfacher Health Check für Load Balancer.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON: Einfacher Status
|
||||||
|
"""
|
||||||
|
return jsonify({'status': 'ok', 'timestamp': datetime.datetime.now().isoformat()})
|
||||||
|
|
||||||
|
@monitoring_bp.route('/metrics')
|
||||||
|
def get_metrics():
|
||||||
|
"""
|
||||||
|
Sammelt und gibt Performance-Metriken zurück.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON: System- und Anwendungsmetriken
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# System-Metriken
|
||||||
|
cpu_percent = psutil.cpu_percent(interval=1)
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
disk = psutil.disk_usage('.')
|
||||||
|
|
||||||
|
# Uptime berechnen
|
||||||
|
uptime = datetime.datetime.now() - metrics['startup_time']
|
||||||
|
|
||||||
|
# Anwendungsmetriken
|
||||||
|
app_metrics = {
|
||||||
|
'system': {
|
||||||
|
'cpu_percent': cpu_percent,
|
||||||
|
'memory_percent': memory.percent,
|
||||||
|
'disk_percent': (disk.used / disk.total) * 100,
|
||||||
|
'uptime_seconds': uptime.total_seconds()
|
||||||
|
},
|
||||||
|
'application': {
|
||||||
|
'requests_total': dict(metrics['requests_total']),
|
||||||
|
'database_queries_total': metrics['database_queries'],
|
||||||
|
'active_jobs': metrics['active_jobs'],
|
||||||
|
'error_count': dict(metrics['error_count']),
|
||||||
|
'startup_time': metrics['startup_time'].isoformat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(app_metrics)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Fehler beim Sammeln der Metriken: {e}")
|
||||||
|
return jsonify({'error': 'Metriken nicht verfügbar'}), 500
|
||||||
|
|
||||||
|
@monitoring_bp.route('/info')
|
||||||
|
def get_info():
|
||||||
|
"""
|
||||||
|
Gibt allgemeine Informationen über die Anwendung zurück.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON: Anwendungsinformationen
|
||||||
|
"""
|
||||||
|
return jsonify({
|
||||||
|
'application': 'MYP Backend',
|
||||||
|
'version': '2.0.0',
|
||||||
|
'flask_env': current_app.config.get('FLASK_ENV', 'unknown'),
|
||||||
|
'debug': current_app.debug,
|
||||||
|
'startup_time': metrics['startup_time'].isoformat(),
|
||||||
|
'python_version': os.sys.version,
|
||||||
|
'config': {
|
||||||
|
'database': current_app.config.get('DATABASE'),
|
||||||
|
'job_check_interval': current_app.config.get('JOB_CHECK_INTERVAL'),
|
||||||
|
'security_enabled': current_app.config.get('SECURITY_ENABLED', False),
|
||||||
|
'rate_limit_enabled': current_app.config.get('RATE_LIMIT_ENABLED', False)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
def record_request_metric(endpoint, method, status_code, duration):
|
||||||
|
"""
|
||||||
|
Zeichnet Request-Metriken auf.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
endpoint: API-Endpunkt
|
||||||
|
method: HTTP-Methode
|
||||||
|
status_code: HTTP-Status-Code
|
||||||
|
duration: Request-Dauer in Sekunden
|
||||||
|
"""
|
||||||
|
key = f"{method}_{endpoint}"
|
||||||
|
metrics['requests_total'][key] += 1
|
||||||
|
metrics['request_duration'][key].append(duration)
|
||||||
|
|
||||||
|
if status_code >= 400:
|
||||||
|
metrics['error_count'][str(status_code)] += 1
|
||||||
|
|
||||||
|
def record_database_query():
|
||||||
|
"""Zeichnet eine Datenbankabfrage auf."""
|
||||||
|
metrics['database_queries'] += 1
|
||||||
|
|
||||||
|
def update_active_jobs(count):
|
||||||
|
"""
|
||||||
|
Aktualisiert die Anzahl aktiver Jobs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: Anzahl aktiver Jobs
|
||||||
|
"""
|
||||||
|
metrics['active_jobs'] = count
|
||||||
|
|
||||||
|
class RequestMetricsMiddleware:
|
||||||
|
"""Middleware für automatisches Request-Tracking."""
|
||||||
|
|
||||||
|
def __init__(self, app=None):
|
||||||
|
self.app = app
|
||||||
|
if app is not None:
|
||||||
|
self.init_app(app)
|
||||||
|
|
||||||
|
def init_app(self, app):
|
||||||
|
"""Initialisiert die Middleware mit der Flask-App."""
|
||||||
|
app.before_request(self.before_request)
|
||||||
|
app.after_request(self.after_request)
|
||||||
|
|
||||||
|
def before_request(self):
|
||||||
|
"""Startet die Zeitmessung für den Request."""
|
||||||
|
from flask import g
|
||||||
|
g.start_time = time.time()
|
||||||
|
|
||||||
|
def after_request(self, response):
|
||||||
|
"""Zeichnet Metriken nach dem Request auf."""
|
||||||
|
from flask import g, request
|
||||||
|
|
||||||
|
if hasattr(g, 'start_time'):
|
||||||
|
duration = time.time() - g.start_time
|
||||||
|
record_request_metric(
|
||||||
|
request.endpoint or 'unknown',
|
||||||
|
request.method,
|
||||||
|
response.status_code,
|
||||||
|
duration
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Globale Middleware-Instanz
|
||||||
|
request_metrics = RequestMetricsMiddleware()
|
@ -23,6 +23,7 @@ requests==2.31.0
|
|||||||
# Monitoring und Logging
|
# Monitoring und Logging
|
||||||
flask-healthcheck==0.1.0
|
flask-healthcheck==0.1.0
|
||||||
prometheus-flask-exporter==0.23.0
|
prometheus-flask-exporter==0.23.0
|
||||||
|
psutil==5.9.6
|
||||||
|
|
||||||
# Entwicklung und Testing (optional)
|
# Entwicklung und Testing (optional)
|
||||||
pytest==7.4.3
|
pytest==7.4.3
|
||||||
|
@ -1 +1,220 @@
|
|||||||
|
"""
|
||||||
|
Sicherheitsmodule und Middleware für die MYP Flask-Anwendung.
|
||||||
|
Implementiert CSRF-Schutz, Content Security Policy und weitere Sicherheitsmaßnahmen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import request, jsonify, current_app
|
||||||
|
from flask_talisman import Talisman
|
||||||
|
from functools import wraps
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
class SecurityMiddleware:
|
||||||
|
"""Zentrale Sicherheits-Middleware für die Anwendung."""
|
||||||
|
|
||||||
|
def __init__(self, app=None):
|
||||||
|
self.app = app
|
||||||
|
self.rate_limits = defaultdict(lambda: deque())
|
||||||
|
self.failed_attempts = defaultdict(int)
|
||||||
|
self.blocked_ips = set()
|
||||||
|
|
||||||
|
if app is not None:
|
||||||
|
self.init_app(app)
|
||||||
|
|
||||||
|
def init_app(self, app):
|
||||||
|
"""Initialisiert die Sicherheits-Middleware mit der Flask-App."""
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
# Talisman für Content Security Policy und HTTPS-Enforcement
|
||||||
|
if not app.debug:
|
||||||
|
Talisman(
|
||||||
|
app,
|
||||||
|
force_https=False, # In Produktion auf True setzen, wenn HTTPS verfügbar
|
||||||
|
strict_transport_security=True,
|
||||||
|
content_security_policy={
|
||||||
|
'default-src': "'self'",
|
||||||
|
'script-src': "'self' 'unsafe-inline'",
|
||||||
|
'style-src': "'self' 'unsafe-inline'",
|
||||||
|
'img-src': "'self' data:",
|
||||||
|
'font-src': "'self'",
|
||||||
|
'connect-src': "'self'",
|
||||||
|
'form-action': "'self'"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Request-Hooks registrieren
|
||||||
|
app.before_request(self.before_request_security_check)
|
||||||
|
app.after_request(self.after_request_security_headers)
|
||||||
|
|
||||||
|
def before_request_security_check(self):
|
||||||
|
"""Sicherheitsüberprüfungen vor jeder Anfrage."""
|
||||||
|
client_ip = self.get_client_ip()
|
||||||
|
|
||||||
|
# Blocked IPs prüfen
|
||||||
|
if client_ip in self.blocked_ips:
|
||||||
|
current_app.logger.warning(f"Blockierte IP-Adresse versucht Zugriff: {client_ip}")
|
||||||
|
return jsonify({'message': 'Zugriff verweigert'}), 403
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
if self.is_rate_limited(client_ip):
|
||||||
|
current_app.logger.warning(f"Rate Limit überschritten für IP: {client_ip}")
|
||||||
|
return jsonify({'message': 'Zu viele Anfragen'}), 429
|
||||||
|
|
||||||
|
# Content-Length prüfen (Schutz vor großen Payloads)
|
||||||
|
if request.content_length and request.content_length > 10 * 1024 * 1024: # 10MB
|
||||||
|
current_app.logger.warning(f"Payload zu groß von IP: {client_ip}")
|
||||||
|
return jsonify({'message': 'Payload zu groß'}), 413
|
||||||
|
|
||||||
|
def after_request_security_headers(self, response):
|
||||||
|
"""Fügt Sicherheits-Header zu jeder Antwort hinzu."""
|
||||||
|
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
response.headers['X-Frame-Options'] = 'DENY'
|
||||||
|
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||||
|
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
||||||
|
|
||||||
|
# Cache-Control für statische Ressourcen
|
||||||
|
if request.endpoint and 'static' in request.endpoint:
|
||||||
|
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||||
|
else:
|
||||||
|
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||||
|
response.headers['Pragma'] = 'no-cache'
|
||||||
|
response.headers['Expires'] = '0'
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_client_ip(self):
|
||||||
|
"""Ermittelt die Client-IP-Adresse."""
|
||||||
|
if request.headers.get('X-Forwarded-For'):
|
||||||
|
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
|
||||||
|
elif request.headers.get('X-Real-IP'):
|
||||||
|
return request.headers.get('X-Real-IP')
|
||||||
|
else:
|
||||||
|
return request.remote_addr
|
||||||
|
|
||||||
|
def is_rate_limited(self, ip, max_requests=100, window_minutes=15):
|
||||||
|
"""
|
||||||
|
Überprüft Rate Limiting für eine IP-Adresse.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ip: Client-IP-Adresse
|
||||||
|
max_requests: Maximale Anzahl Requests pro Zeitfenster
|
||||||
|
window_minutes: Zeitfenster in Minuten
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn Rate Limit überschritten
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
window_start = now - timedelta(minutes=window_minutes)
|
||||||
|
|
||||||
|
# Alte Einträge entfernen
|
||||||
|
while self.rate_limits[ip] and self.rate_limits[ip][0] < window_start:
|
||||||
|
self.rate_limits[ip].popleft()
|
||||||
|
|
||||||
|
# Neue Anfrage hinzufügen
|
||||||
|
self.rate_limits[ip].append(now)
|
||||||
|
|
||||||
|
# Rate Limit prüfen
|
||||||
|
if len(self.rate_limits[ip]) > max_requests:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def record_failed_login(self, ip):
|
||||||
|
"""
|
||||||
|
Zeichnet fehlgeschlagene Login-Versuche auf.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ip: Client-IP-Adresse
|
||||||
|
"""
|
||||||
|
self.failed_attempts[ip] += 1
|
||||||
|
|
||||||
|
# Nach 5 fehlgeschlagenen Versuchen temporär blockieren
|
||||||
|
if self.failed_attempts[ip] >= 5:
|
||||||
|
self.blocked_ips.add(ip)
|
||||||
|
current_app.logger.warning(f"IP-Adresse blockiert nach zu vielen fehlgeschlagenen Login-Versuchen: {ip}")
|
||||||
|
|
||||||
|
# Automatisches Entsperren nach 1 Stunde
|
||||||
|
def unblock_ip():
|
||||||
|
time.sleep(3600) # 1 Stunde
|
||||||
|
if ip in self.blocked_ips:
|
||||||
|
self.blocked_ips.remove(ip)
|
||||||
|
self.failed_attempts[ip] = 0
|
||||||
|
current_app.logger.info(f"IP-Adresse automatisch entsperrt: {ip}")
|
||||||
|
|
||||||
|
import threading
|
||||||
|
threading.Thread(target=unblock_ip, daemon=True).start()
|
||||||
|
|
||||||
|
def clear_failed_attempts(self, ip):
|
||||||
|
"""
|
||||||
|
Löscht fehlgeschlagene Login-Versuche für eine IP.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ip: Client-IP-Adresse
|
||||||
|
"""
|
||||||
|
if ip in self.failed_attempts:
|
||||||
|
self.failed_attempts[ip] = 0
|
||||||
|
|
||||||
|
def require_api_key(f):
|
||||||
|
"""
|
||||||
|
Decorator für API-Endpunkte, die einen API-Key erfordern.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
f: Zu schützende Funktion
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Geschützte Funktion
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
api_key = request.headers.get('X-API-Key')
|
||||||
|
expected_key = current_app.config.get('API_KEY')
|
||||||
|
|
||||||
|
if not expected_key:
|
||||||
|
# Kein API-Key konfiguriert, Zugriff erlauben
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
return jsonify({'message': 'API-Key erforderlich'}), 401
|
||||||
|
|
||||||
|
# Sichere Vergleichsfunktion verwenden
|
||||||
|
if not hmac.compare_digest(api_key, expected_key):
|
||||||
|
current_app.logger.warning(f"Ungültiger API-Key von IP: {request.remote_addr}")
|
||||||
|
return jsonify({'message': 'Ungültiger API-Key'}), 401
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
def validate_csrf_token():
|
||||||
|
"""
|
||||||
|
Validiert CSRF-Token für POST/PUT/DELETE-Requests.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn Token gültig ist
|
||||||
|
"""
|
||||||
|
if request.method in ['GET', 'HEAD', 'OPTIONS']:
|
||||||
|
return True
|
||||||
|
|
||||||
|
token = request.headers.get('X-CSRF-Token') or request.form.get('csrf_token')
|
||||||
|
session_token = request.cookies.get('csrf_token')
|
||||||
|
|
||||||
|
if not token or not session_token:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return hmac.compare_digest(token, session_token)
|
||||||
|
|
||||||
|
def generate_csrf_token():
|
||||||
|
"""
|
||||||
|
Generiert ein neues CSRF-Token.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: CSRF-Token
|
||||||
|
"""
|
||||||
|
import secrets
|
||||||
|
return secrets.token_hex(32)
|
||||||
|
|
||||||
|
# Globale Sicherheits-Middleware-Instanz
|
||||||
|
security_middleware = SecurityMiddleware()
|
@ -1,86 +1,205 @@
|
|||||||
version: '3'
|
# 🏭 MYP - Manage your Printer (Produktionsumgebung)
|
||||||
|
# Hauptkonfiguration für Container-Orchestrierung
|
||||||
|
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# Backend
|
# === BACKEND SERVICE ===
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
- BUILDKIT_INLINE_CACHE=1
|
||||||
|
image: myp/backend:latest
|
||||||
container_name: myp-backend
|
container_name: myp-backend
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
- SECRET_KEY=${SECRET_KEY:-7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F}
|
# Flask-Konfiguration
|
||||||
|
- FLASK_APP=app.py
|
||||||
|
- FLASK_ENV=${FLASK_ENV:-production}
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Datenbank
|
||||||
- DATABASE_PATH=${DATABASE_PATH:-instance/myp.db}
|
- DATABASE_PATH=${DATABASE_PATH:-instance/myp.db}
|
||||||
|
|
||||||
|
# Sicherheit
|
||||||
|
- SECRET_KEY=${SECRET_KEY:-7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F}
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-secure-jwt-secret}
|
||||||
|
|
||||||
|
# Drucker-Konfiguration
|
||||||
|
- "PRINTERS=${PRINTERS:-{\"Drucker 1\": {\"ip\": \"192.168.0.100\"}, \"Drucker 2\": {\"ip\": \"192.168.0.101\"}, \"Drucker 3\": {\"ip\": \"192.168.0.102\"}, \"Drucker 4\": {\"ip\": \"192.168.0.103\"}, \"Drucker 5\": {\"ip\": \"192.168.0.104\"}, \"Drucker 6\": {\"ip\": \"192.168.0.106\"}}}"
|
||||||
|
|
||||||
|
# TAPO Smart Plug
|
||||||
- TAPO_USERNAME=${TAPO_USERNAME:-till.tomczak@mercedes-benz.com}
|
- TAPO_USERNAME=${TAPO_USERNAME:-till.tomczak@mercedes-benz.com}
|
||||||
- TAPO_PASSWORD=${TAPO_PASSWORD:-744563017196A}
|
- TAPO_PASSWORD=${TAPO_PASSWORD:-744563017196A}
|
||||||
- "PRINTERS=${PRINTERS:-{\"Printer 1\": {\"ip\": \"192.168.0.100\"}, \"Printer 2\": {\"ip\": \"192.168.0.101\"}, \"Printer 3\": {\"ip\": \"192.168.0.102\"}, \"Printer 4\": {\"ip\": \"192.168.0.103\"}, \"Printer 5\": {\"ip\": \"192.168.0.104\"}, \"Printer 6\": {\"ip\": \"192.168.0.106\"}}}"
|
|
||||||
- FLASK_APP=app.py
|
# Netzwerk
|
||||||
- PYTHONUNBUFFERED=1
|
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- PORT=5000
|
- PORT=5000
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/logs:/app/logs
|
- backend_instance:/app/instance
|
||||||
- ./backend/instance:/app/instance
|
- backend_logs:/app/logs
|
||||||
|
- backend_migrations:/app/migrations
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
myp-network:
|
myp-network:
|
||||||
ipv4_address: 192.168.0.5
|
ipv4_address: 192.168.0.5
|
||||||
|
|
||||||
expose:
|
expose:
|
||||||
- "5000"
|
- "5000"
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--spider", "http://localhost:5000/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.backend.rule=PathPrefix(`/api`)"
|
||||||
|
- "traefik.http.services.backend.loadbalancer.server.port=5000"
|
||||||
|
|
||||||
# Next.js Frontend
|
# === FRONTEND SERVICE ===
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
container_name: myp-rp
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
- BUILDKIT_INLINE_CACHE=1
|
||||||
|
- NODE_ENV=${NODE_ENV:-production}
|
||||||
|
image: myp/frontend:latest
|
||||||
|
container_name: myp-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=${NODE_ENV:-production}
|
||||||
- NEXT_PUBLIC_API_URL=/api
|
- NEXT_TELEMETRY_DISABLED=1
|
||||||
|
- NEXT_PUBLIC_API_URL=${API_BASE_URL:-/api}
|
||||||
|
- PORT=3000
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- frontend_data:/app/.next
|
||||||
|
- frontend_db:/app/db
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- myp-network
|
- myp-network
|
||||||
|
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.frontend.rule=PathPrefix(`/`)"
|
||||||
|
- "traefik.http.services.frontend.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
# Caddy Proxy
|
# === REVERSE PROXY SERVICE ===
|
||||||
caddy:
|
caddy:
|
||||||
image: caddy:2.7-alpine
|
image: caddy:2.7-alpine
|
||||||
container_name: myp-caddy
|
container_name: myp-caddy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
- "2019:2019" # Admin API
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend/docker/caddy/Caddyfile:/etc/caddy/Caddyfile
|
- ./proxy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
- caddy_data:/data
|
- caddy_data:/data
|
||||||
- caddy_config:/config
|
- caddy_config:/config
|
||||||
|
- caddy_logs:/var/log/caddy
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- myp-network
|
- myp-network
|
||||||
|
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
- CADDY_HOST=53.37.211.254
|
- CADDY_HOST=${CADDY_HOST:-53.37.211.254}
|
||||||
- CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net
|
- CADDY_DOMAIN=${CADDY_DOMAIN:-m040tbaraspi001.de040.corpintra.net}
|
||||||
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- frontend
|
- frontend
|
||||||
- backend
|
- backend
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "caddy", "validate", "--config", "/etc/caddy/Caddyfile"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
|
# === PERSISTENTE VOLUMES ===
|
||||||
|
volumes:
|
||||||
|
# Backend-Volumes
|
||||||
|
backend_instance:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: ./backend/instance
|
||||||
|
|
||||||
|
backend_logs:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: ./logs
|
||||||
|
|
||||||
|
backend_migrations:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: ./backend/migrations
|
||||||
|
|
||||||
|
# Frontend-Volumes
|
||||||
|
frontend_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
frontend_db:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: ./frontend/db
|
||||||
|
|
||||||
|
# Proxy-Volumes
|
||||||
|
caddy_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
caddy_config:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
caddy_logs:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
# === NETZWERK-KONFIGURATION ===
|
||||||
networks:
|
networks:
|
||||||
myp-network:
|
myp-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
@ -89,7 +208,29 @@ networks:
|
|||||||
config:
|
config:
|
||||||
- subnet: 192.168.0.0/24
|
- subnet: 192.168.0.0/24
|
||||||
gateway: 192.168.0.1
|
gateway: 192.168.0.1
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.enable_ipv6: "false"
|
||||||
|
com.docker.network.bridge.enable_ip_masquerade: "true"
|
||||||
|
com.docker.network.bridge.enable_icc: "true"
|
||||||
|
com.docker.network.bridge.host_binding_ipv4: "0.0.0.0"
|
||||||
|
labels:
|
||||||
|
- "description=MYP Anwendungs-Netzwerk"
|
||||||
|
- "project=myp"
|
||||||
|
- "environment=${NODE_ENV:-production}"
|
||||||
|
|
||||||
volumes:
|
# === KONFIGURATIONSEXTENSIONEN ===
|
||||||
caddy_data:
|
x-logging: &default-logging
|
||||||
caddy_config:
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
labels: "service,environment"
|
||||||
|
|
||||||
|
x-restart-policy: &default-restart-policy
|
||||||
|
unless-stopped
|
||||||
|
|
||||||
|
x-healthcheck-defaults: &default-healthcheck
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
Loading…
x
Reference in New Issue
Block a user