Compare commits

...

12 Commits

Author SHA1 Message Date
Aiirondev_dev d567ba583b Update port configuration for multi-tenant deployment to avoid conflicts
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 13:21:34 +02:00
Aiirondev_dev 5a5af5375d Add missing Nginx configuration for SSL and proxy settings
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 11:17:33 +02:00
Aiirondev_dev a60eb6eebf Update health check endpoint in verify_stack_health function in start.sh and update.sh
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 10:55:29 +02:00
Aiirondev_dev 23b7a4cd2f Remove obsolete configuration file config.yml 2026-04-26 21:38:02 +02:00
Aiirondev_dev e6fd485049 Update service configuration in config.yml and simplify cloudflared command in docker-compose 2026-04-26 17:01:20 +02:00
Aiirondev_dev 15d9eba987 Fix server block syntax in Nginx configuration in update.sh 2026-04-26 16:34:42 +02:00
Aiirondev_dev 05d6d299da Enhance scheduler lock file handling and add health check endpoint 2026-04-26 16:17:29 +02:00
Aiirondev_dev ab9db1211c Add server cleanup function to update.sh for Docker and log management
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 15:51:14 +02:00
Aiirondev_dev 45b69e5ddb Update HTTPS port configuration and add cloudflared setup in Docker Compose 2026-04-26 15:35:29 +02:00
Aiirondev_dev 4971bc859b Add disk space checks and cleanup functions in start.sh and update.sh 2026-04-24 21:29:39 +02:00
Aiirondev_dev b7f55b0de0 Remove custom command for MongoDB service in Docker Compose 2026-04-24 21:18:53 +02:00
Aiirondev_dev 9c24e79bba Add retry mechanism for Docker Compose startup in start.sh 2026-04-24 21:13:31 +02:00
12 changed files with 228 additions and 159 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
NUITKA_BUILD=0
INVENTAR_HTTP_PORT=80
INVENTAR_HTTPS_PORT=443
INVENTAR_HTTPS_PORT=8000
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:latest
+1 -1
View File
@@ -14,7 +14,7 @@ Die optimierte Multi-Tenant-Architektur unterstützt **mehrere isolierte Instanz
└─────────────────────────────────────────────────────────────┘
↓ ↓ ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ App :8001 │ │ App :8002 │ │ App :8003
│ App :10000 │ │ App :10002 │ │ App :10004
│ schule1 │ │ schule2 │ │ schule3 │
│ Tenant: t1 │ │ Tenant: t2 │ │ Tenant: t3 │
│ 20 Users │ │ 20 Users │ │ 20 Users │
+16 -4
View File
@@ -1313,9 +1313,17 @@ def _initialize_scheduler():
if lock_age > 300: # 5 minutes - indicates a stale lock from a previous container run
os.remove(scheduler_lock_path)
app.logger.info(f"Removed stale scheduler lock file (age: {lock_age:.0f}s)")
except Exception:
pass # If we can't clean up, continue anyway
except Exception as e:
app.logger.warning(f"Could not clean up scheduler lock file: {e}")
# Always try to remove lock file on startup (extra safety)
try:
if os.path.exists(scheduler_lock_path):
os.remove(scheduler_lock_path)
app.logger.info("Scheduler lock file removed on startup.")
except Exception as e:
app.logger.warning(f"Could not remove scheduler lock file on startup: {e}")
try:
# Try to create the lock file - only succeeds if it doesn't exist
lock_fd = os.open(scheduler_lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
@@ -1324,7 +1332,7 @@ def _initialize_scheduler():
except FileExistsError:
should_start = False
app.logger.warning("Scheduler lock exists - another process is already running the scheduler")
if should_start:
scheduler.add_job(func=create_daily_backup, trigger="interval", hours=cfg.BACKUP_INTERVAL_HOURS)
scheduler.add_job(func=update_appointment_statuses, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
@@ -10806,6 +10814,10 @@ def get_optimal_image_quality(img, target_size_kb=80):
# PUSH NOTIFICATION API ENDPOINTS
# ============================================================================
@app.route('/health')
def health_check():
return 'OK', 200
@app.route('/api/push/subscribe', methods=['POST'])
def subscribe_to_push():
"""
+2 -2
View File
@@ -13,6 +13,6 @@ redis
reportlab
python-barcode
openpyxl
cryptography
cryptography>=42.0.0
pywebpush
py-vapid==1.9.0
py-vapid>=1.9.0
+2 -2
View File
@@ -1,9 +1,9 @@
{
"dbg": false,
"key": "InventarsystemSecureKey2026XYZ789abcdef012",
"ver": "0.0.2",
"ver": "0.6.44",
"host": "0.0.0.0",
"port": 443,
"port": 8000,
"mongodb": {
"host": "localhost",
+4 -7
View File
@@ -13,9 +13,7 @@ services:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
profiles:
- cloudflare
command: ["--config", "/etc/cloudflared/config.yml", "tunnel", "run"]
command: tunnel run homeserver # 'homeserver' must match your tunnel name
volumes:
- ./docker/cloudflared/config.yml:/etc/cloudflared/config.yml:ro
- /etc/cloudflared/credentials.json:/etc/cloudflared/credentials.json:ro
@@ -47,10 +45,10 @@ services:
redis:
condition: service_started
ports:
- "${INVENTAR_HTTP_PORT:-80}:80"
- "${INVENTAR_HTTPS_PORT:-443}:443"
- "${INVENTAR_HTTP_PORT:-10000}:80"
- "${INVENTAR_HTTPS_PORT:-10001}:8000"
volumes:
- ./docker/nginx/multitenant.conf:/etc/nginx/conf.d/default.conf:ro
- ./docker/nginx/multitenant.conf:/etc/nginx/conf.d/default.conf
- ./certs:/etc/nginx/certs:ro
- ./docker/nginx/acme:/etc/nginx/acme:ro
networks:
@@ -85,7 +83,6 @@ services:
image: mongo:7.0
container_name: inventarsystem-mongodb
restart: unless-stopped
command: mongod --wiredTigerCacheSizeGB 2
expose:
- "27017"
volumes:
+32 -93
View File
@@ -1,102 +1,41 @@
version: "3.8"
services:
app:
build: .
container_name: inventory-app
restart: unless-stopped
environment:
- MONGO_URL=mongodb://mongodb:27017/inventar
- REDIS_URL=redis://redis:6379
- BASE_URL=https://inventar.maximiliangruendinger.de #alle öffentliche subdomains
depends_on:
- mongodb
- redis
mongodb:
image: mongo:latest
container_name: mongodb
restart: unless-stopped
volumes:
- mongo_data:/data/db
redis:
image: redis:alpine
container_name: redis
restart: unless-stopped
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
profiles:
- cloudflare
command: ["--config", "/etc/cloudflared/config.yml", "tunnel", "run"]
# Der Tunnel-Name 'homeserver' muss zu deiner credentials.json passen
command: tunnel run homeserver
volumes:
- ./docker/cloudflared/config.yml:/etc/cloudflared/config.yml:ro
- /etc/cloudflared/credentials.json:/etc/cloudflared/credentials.json:ro
- ./config.yml:/etc/cloudflared/config.yml
- ./credentials.json:/etc/cloudflared/credentials.json
depends_on:
nginx:
condition: service_started
nginx:
image: nginx:1.27-alpine
container_name: inventarsystem-nginx
restart: unless-stopped
depends_on:
app:
condition: service_started
ports:
- "${INVENTAR_HTTP_PORT:-80}:80"
- "${INVENTAR_HTTPS_PORT:-443}:443"
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./certs:/etc/nginx/certs:ro
mongodb:
image: mongo:7.0
container_name: inventarsystem-mongodb
restart: unless-stopped
volumes:
- mongodb_data:/data/db
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
interval: 10s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
container_name: inventarsystem-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes", "--save", "60", "1000"]
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 10
app:
build:
context: .
dockerfile: Dockerfile
working_dir: /app/Web
command: ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "30", "--graceful-timeout", "20", "--max-requests", "200", "--max-requests-jitter", "50", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
container_name: inventarsystem-app
restart: unless-stopped
security_opt:
- no-new-privileges:true
depends_on:
mongodb:
condition: service_healthy
redis:
condition: service_healthy
environment:
INVENTAR_MONGODB_HOST: mongodb
INVENTAR_MONGODB_PORT: "27017"
INVENTAR_MONGODB_DB: Inventarsystem
INVENTAR_REDIS_HOST: redis
INVENTAR_REDIS_PORT: "6379"
INVENTAR_REDIS_CACHE_DB: "1"
INVENTAR_NOTIFICATION_STATUS_CACHE_TTL: "8"
INVENTAR_BACKUP_FOLDER: /data/backups
INVENTAR_LOGS_FOLDER: /data/logs
INVENTAR_DELETED_ARCHIVE_FOLDER: /data/deleted-archives
expose:
- "8000"
volumes:
- ./config.json:/app/config.json:ro
- app_uploads:/app/Web/uploads
- app_thumbnails:/app/Web/thumbnails
- app_previews:/app/Web/previews
- app_qrcodes:/app/Web/QRCodes
- app_backups:/data/backups
- app_logs:/data/logs
- app_deleted_archives:/data/deleted-archives
- app
volumes:
mongodb_data:
app_uploads:
app_thumbnails:
app_previews:
app_qrcodes:
app_backups:
app_logs:
app_deleted_archives:
redis_data:
mongo_data:
+2 -2
View File
@@ -473,8 +473,8 @@ EOF
if [ ! -f "$PROJECT_DIR/.docker-build.env" ]; then
cat > "$TMP_DIR/.docker-build.env" <<EOF
NUITKA_BUILD=0
INVENTAR_HTTP_PORT=80
INVENTAR_HTTPS_PORT=443
INVENTAR_HTTP_PORT=10000
INVENTAR_HTTPS_PORT=10001
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:$tag
EOF
sudo install -m 644 "$TMP_DIR/.docker-build.env" "$PROJECT_DIR/.docker-build.env"
+2 -2
View File
@@ -12,6 +12,6 @@ redis
reportlab
python-barcode
openpyxl
cryptography
cryptography>=42.0.0
pywebpush
py-vapid==1.9.0
py-vapid>=1.9.0
+7
View File
@@ -1,4 +1,11 @@
#!/bin/bash
# Nur die App neu bauen und starten, ohne den Tunnel oder die DB zu killen
docker compose up -d --build app
# Optional: Alles aufräumen, was nicht mehr gebraucht wird
docker image prune -f
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+71 -41
View File
@@ -15,12 +15,15 @@ if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
fi
NUITKA_BUILD_VALUE="0"
HTTP_PORT_VALUE="8001"
HTTPS_PORT_VALUE="8443"
HTTP_PORT_VALUE="10000"
HTTPS_PORT_VALUE="10001"
DEFAULT_TENANT_PORT_START="${INVENTAR_TENANT_PORT_START:-10000}"
DEFAULT_TENANT_TLS_OFFSET="${INVENTAR_TENANT_TLS_OFFSET:-1}"
CRON_SETUP_VALUE="${INVENTAR_SETUP_CRON:-1}"
APP_IMAGE_VALUE="${INVENTAR_APP_IMAGE:-$APP_IMAGE_REPO:latest}"
COMPOSE_FILE="docker-compose-multitenant.yml"
COMPOSE_PROFILES_VALUE=""
MIN_DOCKER_FREE_MB="${INVENTAR_MIN_DOCKER_FREE_MB:-1024}"
usage() {
cat <<EOF
@@ -508,57 +511,79 @@ stop_host_nginx_services() {
return 1
}
find_free_port_pair() {
local port="${1:-$DEFAULT_TENANT_PORT_START}"
while port_in_use "$port" || port_in_use "$((port + DEFAULT_TENANT_TLS_OFFSET))"; do
port=$((port + 2))
done
echo "$port"
}
configure_host_ports() {
local requested_http requested_https
local requested_http requested_https base_port
requested_http=""
if [ -f "$ENV_FILE" ]; then
requested_http="$(awk -F= '/^INVENTAR_HTTP_PORT=/{print $2}' "$ENV_FILE" | tr -d ' ' || true)"
fi
if [ -z "$requested_http" ]; then
requested_http="8001"
fi
if stack_owns_host_port "$requested_http" "80"; then
HTTP_PORT_VALUE="$requested_http"
elif port_in_use "$requested_http"; then
if ! port_in_use "$requested_http"; then
HTTP_PORT_VALUE="$requested_http"
echo "Freed HTTP port $requested_http by stopping host nginx service"
else
HTTP_PORT_VALUE="$(find_free_port 8080)"
echo "HTTP port is in use. Using fallback HTTP port: $HTTP_PORT_VALUE"
fi
else
HTTP_PORT_VALUE="$requested_http"
fi
requested_https=""
if [ -f "$ENV_FILE" ]; then
requested_http="$(awk -F= '/^INVENTAR_HTTP_PORT=/{print $2}' "$ENV_FILE" | tr -d ' ' || true)"
requested_https="$(awk -F= '/^INVENTAR_HTTPS_PORT=/{print $2}' "$ENV_FILE" | tr -d ' ' || true)"
fi
if [ -z "$requested_http" ]; then
requested_http="$DEFAULT_TENANT_PORT_START"
fi
if [ -z "$requested_https" ]; then
requested_https="8443"
requested_https="$((requested_http + DEFAULT_TENANT_TLS_OFFSET))"
fi
if stack_owns_host_port "$requested_https" "443"; then
if stack_owns_host_port "$requested_http" "80" && stack_owns_host_port "$requested_https" "443"; then
HTTP_PORT_VALUE="$requested_http"
HTTPS_PORT_VALUE="$requested_https"
elif port_in_use "$requested_https"; then
return
fi
if ! port_in_use "$requested_https"; then
HTTPS_PORT_VALUE="$requested_https"
echo "Freed HTTPS port $requested_https by stopping host nginx service"
return
fi
HTTPS_PORT_VALUE="$(find_free_port 8443)"
echo "HTTPS port is in use. Using fallback HTTPS port: $HTTPS_PORT_VALUE"
else
if ! port_in_use "$requested_http" && ! port_in_use "$requested_https"; then
HTTP_PORT_VALUE="$requested_http"
HTTPS_PORT_VALUE="$requested_https"
return
fi
base_port="$(find_free_port_pair "$DEFAULT_TENANT_PORT_START")"
HTTP_PORT_VALUE="$base_port"
HTTPS_PORT_VALUE="$((base_port + DEFAULT_TENANT_TLS_OFFSET))"
echo "Tenant host ports are already occupied. Assigned new tenant ports: $HTTP_PORT_VALUE and $HTTPS_PORT_VALUE"
}
ensure_min_docker_disk_space() {
local docker_root available_kb available_mb
if ! command -v df >/dev/null 2>&1; then
return 0
fi
docker_root="$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || true)"
if [ -z "$docker_root" ]; then
docker_root="/var/lib/docker"
fi
if [ ! -d "$docker_root" ]; then
return 0
fi
available_kb="$(df -Pk "$docker_root" 2>/dev/null | awk 'NR==2 {print $4}' || true)"
if [ -z "$available_kb" ]; then
return 0
fi
available_mb=$((available_kb / 1024))
if [ "$available_mb" -lt "$MIN_DOCKER_FREE_MB" ]; then
echo "Error: low disk space in Docker data root ($docker_root)."
echo "Available: ${available_mb} MB; required minimum: ${MIN_DOCKER_FREE_MB} MB"
echo "MongoDB may fail with 'No space left on device'. Free space and retry."
exit 1
fi
}
@@ -599,7 +624,7 @@ verify_stack_health() {
printf '%s\n' "$running_services" | grep -Fxq nginx && \
printf '%s\n' "$running_services" | grep -Fxq mongodb; then
if docker compose "${compose_args[@]}" exec -T app python3 -c "import flask, pymongo" >/dev/null 2>&1; then
if curl -kfsS "https://127.0.0.1:$HTTPS_PORT_VALUE" >/dev/null 2>&1; then
if curl -kfsSL "https://127.0.0.1:$HTTPS_PORT_VALUE/health" >/dev/null 2>&1; then
echo "Health check passed."
return 0
fi
@@ -640,6 +665,7 @@ configure_nuitka_mode
resolve_app_image
configure_cloudflared_profile
configure_host_ports
ensure_min_docker_disk_space
ensure_app_image_loaded
write_env_file
write_runtime_compose_override
@@ -653,7 +679,11 @@ compose_up_args+=(--env-file "$ENV_FILE")
if [ -n "$COMPOSE_PROFILES_VALUE" ]; then
export COMPOSE_PROFILES="$COMPOSE_PROFILES_VALUE"
fi
docker compose "${compose_up_args[@]}" up -d --remove-orphans
if ! docker compose "${compose_up_args[@]}" up -d --remove-orphans; then
echo "Docker Compose startup failed once. Waiting briefly and retrying..."
sleep 5
docker compose "${compose_up_args[@]}" up -d --remove-orphans
fi
verify_stack_health
+88 -4
View File
@@ -16,6 +16,8 @@ ENV_FILE="$PROJECT_DIR/.docker-build.env"
APP_IMAGE_REPO="ghcr.io/aiirondev/legendary-octo-garbanzo"
DIST_DIR="$PROJECT_DIR/dist"
COMPOSE_FILE="docker-compose-multitenant.yml"
MIN_ROOT_FREE_MB="${INVENTAR_MIN_ROOT_FREE_MB:-2048}"
DIST_KEEP_COUNT="${INVENTAR_DIST_KEEP_COUNT:-2}"
mkdir -p "$LOG_DIR"
chmod 777 "$LOG_DIR" 2>/dev/null || true
@@ -149,6 +151,65 @@ EOF
fi
}
ensure_min_root_disk_space() {
local available_kb available_mb
if ! command -v df >/dev/null 2>&1; then
return 0
fi
available_kb="$(df -Pk "$PROJECT_DIR" 2>/dev/null | awk 'NR==2 {print $4}' || true)"
if [ -z "$available_kb" ]; then
return 0
fi
available_mb=$((available_kb / 1024))
if [ "$available_mb" -lt "$MIN_ROOT_FREE_MB" ]; then
log_message "ERROR: Low disk space on filesystem containing $PROJECT_DIR"
log_message "Available: ${available_mb} MB; required minimum: ${MIN_ROOT_FREE_MB} MB"
log_message "Free disk space and rerun update."
exit 1
fi
}
cleanup_old_dist_artifacts() {
local keep_count
keep_count="$DIST_KEEP_COUNT"
if [ ! -d "$DIST_DIR" ]; then
return 0
fi
if ! [[ "$keep_count" =~ ^[0-9]+$ ]]; then
keep_count=2
fi
mapfile -t archives < <(find "$DIST_DIR" -maxdepth 1 -type f \( -name 'inventarsystem-image-*.tar.gz' -o -name 'inventarsystem-image-*.tar' \) -printf '%T@ %p\n' | sort -nr | awk '{print $2}')
if [ "${#archives[@]}" -le "$keep_count" ]; then
return 0
fi
local index old_archive deleted=0
for (( index=keep_count; index<${#archives[@]}; index++ )); do
old_archive="${archives[$index]}"
if rm -f "$old_archive"; then
deleted=$((deleted + 1))
fi
done
if [ "$deleted" -gt 0 ]; then
log_message "Cleaned up $deleted old dist image archive(s)"
fi
}
cleanup_docker_dangling_images() {
if docker image prune -f >> "$LOG_FILE" 2>&1; then
log_message "Cleaned up dangling Docker images"
else
log_message "WARNING: Could not prune dangling Docker images"
fi
}
usage() {
cat <<EOF
Usage: $0 [options]
@@ -408,8 +469,8 @@ deploy() {
if [ ! -f "$ENV_FILE" ]; then
cat > "$ENV_FILE" <<EOF
NUITKA_BUILD=0
INVENTAR_HTTP_PORT=80
INVENTAR_HTTPS_PORT=443
INVENTAR_HTTP_PORT=10000
INVENTAR_HTTPS_PORT=10001
INVENTAR_APP_IMAGE=$app_image
EOF
elif grep -q '^INVENTAR_APP_IMAGE=' "$ENV_FILE"; then
@@ -447,8 +508,8 @@ verify_stack_health() {
if printf '%s\n' "$running_services" | grep -Fxq app && \
printf '%s\n' "$running_services" | grep -Fxq nginx && \
printf '%s\n' "$running_services" | grep -Fxq mongodb; then
# Primary check: HTTP endpoint responds (most reliable)
if curl -kfsS "https://127.0.0.1:$https_port" >/dev/null 2>&1; then
# Primary check: health endpoint responds (most reliable)
if curl -kfsSL "https://127.0.0.1:$https_port/health" >/dev/null 2>&1; then
return 0
fi
fi
@@ -460,9 +521,29 @@ verify_stack_health() {
return 1
}
cleanup_server_space() {
log_message "Running server cleanup before update..."
# Remove unused Docker objects
if docker system prune -af --volumes >> "$LOG_FILE" 2>&1; then
log_message "Docker system pruned (all unused images, containers, volumes, networks)"
else
log_message "WARNING: Docker system prune failed"
fi
# Clean up old dist artifacts
cleanup_old_dist_artifacts
# Clean up log files older than 7 days
if find "$LOG_DIR" -type f -name '*.log' -mtime +7 -exec rm -f {} +; then
log_message "Old log files (older than 7 days) cleaned up"
else
log_message "WARNING: Failed to clean up old log files"
fi
}
main() {
parse_args "$@"
cleanup_server_space
ensure_runtime_dependencies
ensure_tls_certificates
ensure_nginx_config_mount_source
@@ -472,6 +553,7 @@ main() {
require_cmd docker
require_cmd python3
ensure_min_root_disk_space
archive_logs
create_backup
@@ -559,6 +641,8 @@ main() {
fi
echo "$latest_tag" > "$STATE_FILE"
cleanup_old_dist_artifacts
cleanup_docker_dangling_images
log_message "Update completed successfully to release $latest_tag"
}