feat(config): TZ + NTP_SERVER in .env mit sinnvollen Defaults

- .env / .env.example: TZ=Europe/Berlin und NTP_SERVER=ptbtime1.ptb.de
  (offizielle deutsche Zeitreferenz, hohe Verfuegbarkeit)
- app/__init__.py setzt prozessweite Zeitzone frueh via os.environ+tzset
- Leichtgewichtiger SNTP-Client (pure socket, keine deps) prueft den
  Uhr-Offset beim Start im Hintergrund-Thread und warnt bei Abweichung >5s
- Dockerfile installiert tzdata und ENV TZ=Europe/Berlin als Fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-14 15:15:57 +02:00
parent ba3e619963
commit dca064427e
5 changed files with 104 additions and 0 deletions

View File

@ -31,6 +31,18 @@ FRONTEND_URL=https://cloud.example.com
# Max Upload-Groesse in MB
MAX_UPLOAD_SIZE_MB=500
# Zeitzone (prozessweit) - z.B. Europe/Berlin, Europe/Vienna, UTC
# Wirkt auf datetime.now(), strftime %Z und Kalender/Task-Zeitstempel.
TZ=Europe/Berlin
# NTP-Server zum Pruefen der Uhrzeit beim Start (nicht-invasiver Offset-Check
# - im Container kann die Systemuhr nicht gesetzt werden; bei Abweichung >5s
# erscheint eine Warnung im Log, dann bitte die Host-Uhr synchronisieren).
# Leerlassen um den Check zu deaktivieren.
# Default: Physikalisch-Technische Bundesanstalt (offizielle deutsche Zeit).
# Alternativen: ptbtime2.ptb.de, ptbtime3.ptb.de, de.pool.ntp.org, time.cloudflare.com
NTP_SERVER=ptbtime1.ptb.de
# OnlyOffice Document Server (optional)
# Eigene Subdomain mit HTTPS, z.B. https://office.example.com
# JWT wird automatisch vom JWT_SECRET_KEY oben verwendet

View File

@ -13,6 +13,7 @@ WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
tzdata \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
@ -30,6 +31,7 @@ RUN mkdir -p /app/data/files
# Environment
ENV FLASK_ENV=production
ENV TZ=Europe/Berlin
ENV DATABASE_PATH=/app/data/minicloud.db
ENV UPLOAD_PATH=/app/data/files

View File

@ -1,4 +1,5 @@
import os
import time
from pathlib import Path
from flask import Flask, Response, redirect, send_from_directory
@ -8,6 +9,20 @@ from app.config import Config
from app.extensions import db, bcrypt, migrate
def _configure_timezone(tz_name: str) -> None:
"""Prozess-Zeitzone setzen, sodass datetime.now(), strftime %Z etc.
die konfigurierte TZ verwenden. Sichere no-op wenn tzdata fehlt."""
if not tz_name:
return
os.environ['TZ'] = tz_name
tzset = getattr(time, 'tzset', None)
if tzset:
try:
tzset()
except Exception:
pass
def _auto_migrate(db):
"""Add missing columns to existing tables by comparing model definitions
with actual database schema. This handles the case where new columns are
@ -61,6 +76,9 @@ def _auto_migrate(db):
def create_app(config_class=Config):
# Zeitzone moeglichst frueh setzen - vor allen datetime.now()-Aufrufen
_configure_timezone(getattr(config_class, 'TIMEZONE', None) or os.environ.get('TZ'))
# Check if static frontend build exists (Docker production mode)
static_dir = Path(__file__).resolve().parent.parent / 'static'
if static_dir.exists():
@ -171,4 +189,15 @@ def create_app(config_class=Config):
from app.services.backup_scheduler import start_backup_scheduler
start_backup_scheduler(app)
# NTP-Offset gegen den konfigurierten Zeitserver pruefen (nicht fatal).
ntp_server = app.config.get('NTP_SERVER') or ''
if ntp_server.strip():
import threading
from app.services.ntp_check import check_and_log
threading.Thread(
target=check_and_log,
args=(ntp_server.strip(), app.logger),
daemon=True,
).start()
return app

View File

@ -40,3 +40,8 @@ class Config:
# CORS
FRONTEND_URL = os.environ.get('FRONTEND_URL', 'http://localhost:3000')
# Zeitzone (prozessweit, wirkt nach time.tzset())
TIMEZONE = os.environ.get('TZ', 'Europe/Berlin')
# NTP-Server fuer Offset-Check beim Start. Leerstring deaktiviert den Check.
NTP_SERVER = os.environ.get('NTP_SERVER', 'ptbtime1.ptb.de')

View File

@ -0,0 +1,56 @@
"""Leichtgewichtiger SNTP-Client zum Pruefen des Zeit-Offsets.
Im Container koennen wir die Systemzeit nicht wirklich setzen (braucht
CAP_SYS_TIME). Aber wir koennen den Offset ermitteln und loggen, damit
der Admin weiss, ob der Host driftet. Fuer einen harten Sync muss auf
dem Host selbst ein NTP-Daemon laufen.
"""
from __future__ import annotations
import socket
import struct
import time
_NTP_EPOCH_OFFSET = 2208988800 # Sekunden zwischen 1900 und 1970
def query_ntp(server: str, timeout: float = 3.0, port: int = 123) -> float | None:
"""Fragt einen NTP-Server und gibt das Offset (Server - Local) in
Sekunden zurueck, oder None bei Fehler."""
packet = b'\x1b' + 47 * b'\0' # LI=0, VN=3, Mode=3 (client)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout)
try:
t0 = time.time()
sock.sendto(packet, (server, port))
data, _ = sock.recvfrom(1024)
t3 = time.time()
except (socket.gaierror, socket.timeout, OSError):
return None
finally:
sock.close()
if len(data) < 48:
return None
# Transmit timestamp: Offset 40, 8 bytes, fixed point 32.32
secs, frac = struct.unpack('!II', data[40:48])
if secs == 0:
return None
t2 = secs - _NTP_EPOCH_OFFSET + frac / 2**32
# Einfacher Offset (sans roundtrip): (t2 - ((t0 + t3) / 2))
return t2 - (t0 + t3) / 2
def check_and_log(server: str, logger=None) -> float | None:
import logging
log = logger or logging.getLogger('ntp')
offset = query_ntp(server)
if offset is None:
log.warning('NTP-Check: Server %s nicht erreichbar', server)
return None
if abs(offset) > 5.0:
log.warning('NTP-Check: Systemzeit weicht um %.2fs von %s ab -> Host-Uhr synchronisieren!',
offset, server)
else:
log.info('NTP-Check: Offset %.3fs gegen %s (ok)', offset, server)
return offset