Compare commits

...

15 Commits

Author SHA1 Message Date
Aiirondev_dev d2b3d4f6ea Enhance tenant management script: improve tenant listing functionality and handle missing config file 2026-04-28 18:44:47 +02:00
Aiirondev_dev f3d327f28f Enhance tenant management scripts: add runtime port update functionality and handle occupied ports 2026-04-28 18:18:04 +02:00
Aiirondev_dev 2b05d8c952 Refactor manage-tenant script: improve parameter handling and ensure default values 2026-04-28 17:54:43 +02:00
Aiirondev_dev 6cb41c1277 Enhance tenant management script: add help option to display usage instructions 2026-04-28 17:49:44 +02:00
Aiirondev_dev f0d02d6af1 Refactor restart script: streamline Docker compose argument handling and improve readability 2026-04-28 17:20:10 +02:00
Aiirondev_dev 6f5e24104b Refactor restart script: improve Docker container restart logic and update completion messages 2026-04-28 17:06:37 +02:00
Aiirondev_dev 43406c29d1 Enhance tenant management script: switch to bash, enforce strict mode, and validate port input 2026-04-28 16:54:42 +02:00
Aiirondev_dev 7873c45cfc Refactor multi-tenant deployment: update port handling in scripts, add tenant port registration, and enhance Docker configurations
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 16:36:55 +02:00
Aiirondev_dev 14c4192306 Refactor Docker setup: remove Nginx and TLS configurations, update service dependencies, and adjust health check endpoints
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 15:41:10 +02:00
Aiirondev_dev 1efc7e01be Update HTTP and HTTPS port configuration in .docker-build.env
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 14:38:44 +02:00
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
12 changed files with 459 additions and 429 deletions
+1 -2
View File
@@ -1,4 +1,3 @@
NUITKA_BUILD=0 NUITKA_BUILD=0
INVENTAR_HTTP_PORT=80 INVENTAR_HTTP_PORT=10000
INVENTAR_HTTPS_PORT=8000
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:latest INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:latest
+6
View File
@@ -0,0 +1,6 @@
services:
app:
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", "-"]
image: ghcr.io/aiirondev/legendary-octo-garbanzo:latest
build: null
+33 -32
View File
@@ -158,42 +158,17 @@ jobs:
- name: Create release-only docker bundle - name: Create release-only docker bundle
run: | run: |
mkdir -p release-bundle mkdir -p release-bundle
mkdir -p release-bundle/docker/nginx
cat > release-bundle/docker-compose.yml <<EOF cat > release-bundle/docker-compose.yml <<EOF
services: services:
nginx:
image: nginx:1.27-alpine
container_name: inventarsystem-nginx
restart: unless-stopped
depends_on:
- app
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
app: app:
image: ${INVENTAR_APP_IMAGE:-ghcr.io/aiirondev/legendary-octo-garbanzo:latest} image: ${INVENTAR_APP_IMAGE:-ghcr.io/aiirondev/legendary-octo-garbanzo:${{ steps.meta.outputs.tag }}}
pull_policy: never
container_name: inventarsystem-app container_name: inventarsystem-app
restart: unless-stopped restart: unless-stopped
ports:
- "${INVENTAR_HTTP_PORT:-10000}:8000"
depends_on: depends_on:
mongodb: - mongodb
condition: service_healthy - redis
environment: environment:
INVENTAR_MONGODB_HOST: mongodb INVENTAR_MONGODB_HOST: mongodb
INVENTAR_MONGODB_PORT: "27017" INVENTAR_MONGODB_PORT: "27017"
@@ -211,6 +186,33 @@ jobs:
- app_backups:/data/backups - app_backups:/data/backups
- app_logs:/data/logs - app_logs:/data/logs
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 --maxmemory 512mb --maxmemory-policy allkeys-lru
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes: volumes:
mongodb_data: mongodb_data:
app_uploads: app_uploads:
@@ -219,9 +221,9 @@ jobs:
app_qrcodes: app_qrcodes:
app_backups: app_backups:
app_logs: app_logs:
redis_data:
EOF EOF
cp docker/nginx/default.conf release-bundle/docker/nginx/default.conf
cp start.sh release-bundle/start.sh cp start.sh release-bundle/start.sh
cp stop.sh release-bundle/stop.sh cp stop.sh release-bundle/stop.sh
cp restart.sh release-bundle/restart.sh cp restart.sh release-bundle/restart.sh
@@ -232,7 +234,6 @@ jobs:
# Multitenant scripts & docs # Multitenant scripts & docs
cp docker-compose-multitenant.yml release-bundle/docker-compose-multitenant.yml cp docker-compose-multitenant.yml release-bundle/docker-compose-multitenant.yml
cp docker/nginx/multitenant.conf release-bundle/docker/nginx/multitenant.conf
cp manage-tenant.sh release-bundle/manage-tenant.sh cp manage-tenant.sh release-bundle/manage-tenant.sh
cp run-tenant-cmd.sh release-bundle/run-tenant-cmd.sh cp run-tenant-cmd.sh release-bundle/run-tenant-cmd.sh
cp MULTITENANT_DEPLOYMENT.md release-bundle/MULTITENANT_DEPLOYMENT.md cp MULTITENANT_DEPLOYMENT.md release-bundle/MULTITENANT_DEPLOYMENT.md
+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 │ │ schule1 │ │ schule2 │ │ schule3 │
│ Tenant: t1 │ │ Tenant: t2 │ │ Tenant: t3 │ │ Tenant: t1 │ │ Tenant: t2 │ │ Tenant: t3 │
│ 20 Users │ │ 20 Users │ │ 20 Users │ │ 20 Users │ │ 20 Users │ │ 20 Users │
+57 -4
View File
@@ -10,6 +10,8 @@ Each tenant can support up to 20+ users with isolated data and resource pools.
from flask import request, g, has_request_context from flask import request, g, has_request_context
from functools import wraps from functools import wraps
import logging import logging
import os
import re
import settings as cfg import settings as cfg
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -30,6 +32,46 @@ def _get_nested_value(source, path, default=None):
return current return current
def _parse_port_from_host(host):
"""Parse host header and return (hostname, port) when a numeric port is present."""
if host.startswith('['):
# IPv6 with port: [::1]:10000
if ']:' in host:
host_part, _, port_part = host.rpartition(']:')
return host_part + ']', port_part
return host, None
if host.count(':') == 1:
hostname, port = host.split(':', 1)
if port.isdigit():
return hostname, port
return host, None
def _tenant_id_for_port(port):
"""Map a host port to a registered tenant ID via tenant configs or env overrides."""
for tenant_id, config in TENANT_REGISTRY.items():
if isinstance(config, dict) and config.get('port') is not None:
try:
configured_port = str(int(config.get('port')))
except (TypeError, ValueError):
continue
if configured_port == str(port):
return tenant_id
port_map = os.getenv('INVENTAR_TENANT_PORT_MAP', '').strip()
if port_map:
for mapping in re.split(r'[;,\s]+', port_map):
if '=' not in mapping:
continue
key, value = mapping.split('=', 1)
if key.strip() == str(port):
return value.strip()
return None
def get_tenant_config(tenant_id=None): def get_tenant_config(tenant_id=None):
"""Return the registered config for a tenant, falling back to default.""" """Return the registered config for a tenant, falling back to default."""
if tenant_id is None: if tenant_id is None:
@@ -52,19 +94,20 @@ def is_tenant_module_enabled(module_name, tenant_id=None, default=False):
class TenantContext: class TenantContext:
""" """
Manages current tenant context for request lifecycle. Manages current tenant context for request lifecycle.
Automatically resolves tenant from subdomain or request header. Automatically resolves tenant from port, header, or subdomain.
""" """
def __init__(self): def __init__(self):
self.tenant_id = None self.tenant_id = None
self.db_name = None self.db_name = None
self.subdomain = None self.subdomain = None
self.port = None
self.config = {} self.config = {}
def resolve_tenant(self): def resolve_tenant(self):
""" """
Resolve tenant from request context. Resolve tenant from request context.
Priority: Header > Subdomain > Default Priority: Header > Port mapping > Subdomain > Default
""" """
if not has_request_context(): if not has_request_context():
return None return None
@@ -76,8 +119,18 @@ class TenantContext:
self.config = get_tenant_config(tenant_from_header) self.config = get_tenant_config(tenant_from_header)
return self._get_db_name(tenant_from_header) return self._get_db_name(tenant_from_header)
# Priority 2: Subdomain extraction # Priority 2: Port-based tenant mapping
host = request.host.lower() host = request.host.lower()
_, port = _parse_port_from_host(host)
self.port = port
if port:
tenant_from_port = _tenant_id_for_port(port)
if tenant_from_port:
self.tenant_id = tenant_from_port
self.config = get_tenant_config(tenant_from_port)
return self._get_db_name(tenant_from_port)
# Priority 3: Subdomain extraction
parts = host.split('.') parts = host.split('.')
# Extract subdomain from host # Extract subdomain from host
@@ -93,7 +146,7 @@ class TenantContext:
self.config = get_tenant_config(potential_subdomain) self.config = get_tenant_config(potential_subdomain)
return self._get_db_name(potential_subdomain) return self._get_db_name(potential_subdomain)
# Fallback to default tenant if no subdomain detected # Fallback to default tenant if no tenant identifier found
self.tenant_id = 'default' self.tenant_id = 'default'
self.config = get_tenant_config('default') self.config = get_tenant_config('default')
return self._get_db_name('default') return self._get_db_name('default')
-13
View File
@@ -1,13 +0,0 @@
tunnel: homeserver
credentials-file: /etc/cloudflared/credentials.json
ingress:
# Repo 1: Inventar
- hostname: inventar.maximiliangruendinger.de
service: http://inventory-app:3000
# Repo 2: Key-Service (Buchung)
- hostname: booking.maximiliangruendinger.de
service: http://key-service-app:8080
- service: http_status:404
+5 -46
View File
@@ -1,30 +1,14 @@
# Multi-Tenant Optimized Docker Compose # Multi-Tenant Optimized Docker Compose
# Supports running multiple isolated app instances per subdomain # Supports running multiple isolated app instances with direct Docker host port binding
# Each instance: ~50MB base + 20-30MB per 20 users # Each instance: ~50MB base + 20-30MB per 20 users
# #
# Usage: # Usage:
# docker-compose -f docker-compose-multitenant.yml up -d # docker-compose -f docker-compose-multitenant.yml up -d
# #
# Scale example: To support 10 tenants with 20 users each: # Example: Start the stack and expose the app on host port 10000
# docker-compose -f docker-compose-multitenant.yml up -d --scale app=10 # INVENTAR_HTTP_PORT=10000 docker-compose -f docker-compose-multitenant.yml up -d
services: services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
profiles:
- cloudflare
command: ["--config", "/etc/cloudflared/config.yml", "tunnel", "run"]
volumes:
- ./docker/cloudflared/config.yml:/etc/cloudflared/config.yml:ro
- /etc/cloudflared/credentials.json:/etc/cloudflared/credentials.json:ro
depends_on:
nginx:
condition: service_started
networks:
- inventar-net
# Management Container for multi-tenant scripts # Management Container for multi-tenant scripts
tenant-manager: tenant-manager:
image: docker:cli image: docker:cli
@@ -37,33 +21,6 @@ services:
- .:/workspace - .:/workspace
entrypoint: ["sh", "-c", "cd /workspace && ./manage-tenant.sh \"$$@\"", "--"] entrypoint: ["sh", "-c", "cd /workspace && ./manage-tenant.sh \"$$@\"", "--"]
nginx:
image: nginx:1.27-alpine
container_name: inventarsystem-nginx
restart: unless-stopped
depends_on:
app:
condition: service_started
redis:
condition: service_started
ports:
- "${INVENTAR_HTTP_PORT:-80}:80"
- "${INVENTAR_HTTPS_PORT:-8000}:8000"
volumes:
- ./docker/nginx/multitenant.conf:/etc/nginx/conf.d/default.conf
- ./certs:/etc/nginx/certs:ro
- ./docker/nginx/acme:/etc/nginx/acme:ro
networks:
- inventar-net
healthcheck:
test: ["CMD", "wget", "-q", "-O-", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
environment:
NGINX_WORKER_PROCESSES: auto
NGINX_WORKER_CONNECTIONS: 1024
redis: redis:
image: redis:7-alpine image: redis:7-alpine
container_name: inventarsystem-redis container_name: inventarsystem-redis
@@ -117,6 +74,8 @@ services:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
ports:
- "${INVENTAR_HTTP_PORT:-10000}:8000"
networks: networks:
- inventar-net - inventar-net
expose: expose:
+2 -2
View File
@@ -473,8 +473,8 @@ EOF
if [ ! -f "$PROJECT_DIR/.docker-build.env" ]; then if [ ! -f "$PROJECT_DIR/.docker-build.env" ]; then
cat > "$TMP_DIR/.docker-build.env" <<EOF cat > "$TMP_DIR/.docker-build.env" <<EOF
NUITKA_BUILD=0 NUITKA_BUILD=0
INVENTAR_HTTP_PORT=80 INVENTAR_HTTP_PORT=10000
INVENTAR_HTTPS_PORT=443 INVENTAR_HTTPS_PORT=10001
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:$tag INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:$tag
EOF EOF
sudo install -m 644 "$TMP_DIR/.docker-build.env" "$PROJECT_DIR/.docker-build.env" sudo install -m 644 "$TMP_DIR/.docker-build.env" "$PROJECT_DIR/.docker-build.env"
+159 -21
View File
@@ -1,4 +1,6 @@
#!/bin/sh #!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# Script to manage multitenant deployment # Script to manage multitenant deployment
# Allows adding, removing, and restarting tenants without downtime for others # Allows adding, removing, and restarting tenants without downtime for others
@@ -7,42 +9,149 @@ if [ ! -f "docker-compose-multitenant.yml" ]; then
exit 1 exit 1
fi fi
CONFIG_FILE="$PWD/config.json"
show_help() { show_help() {
echo "Usage: ./manage-tenant.sh [COMMAND] [OPTIONS]" echo "Usage: ./manage-tenant.sh [COMMAND] [OPTIONS]"
echo "" echo ""
echo "Commands:" echo "Commands:"
echo " add <tenant_id> Add a new tenant (initializes database)" echo " add <tenant_id> [port] Add a new tenant (initializes database)"
echo " remove <tenant_id> Remove a tenant completely (deletes data!)" echo " remove <tenant_id> Remove a tenant completely (deletes data!)"
echo " restart-tenant <id> 'Restart' a single tenant (clears cache/sessions)" echo " restart-tenant <id> 'Restart' a single tenant (clears cache/sessions)"
echo " restart-all Restart all application containers (zero-downtime reload)" echo " restart-all Restart all application containers (zero-downtime reload)"
echo " list List active tenants" echo " list List active tenants"
echo " -h, --help Show this help message"
echo "" echo ""
echo "Examples:" echo "Examples:"
echo " ./manage-tenant.sh add school_a" echo " ./manage-tenant.sh add school_a 10001"
echo " ./manage-tenant.sh remove test_tenant" echo " ./manage-tenant.sh remove test_tenant"
echo " ./manage-tenant.sh restart-all" echo " ./manage-tenant.sh restart-all"
echo " ./manage-tenant.sh -h"
exit 1 exit 1
} }
if [ -z "$1" ]; then register_tenant_port() {
local tenant_id="$1"
local port="$2"
if python3 - <<'PY' "$CONFIG_FILE" "$tenant_id" "$port"
import json, sys, os
path, tenant_id, port_str = sys.argv[1], sys.argv[2], sys.argv[3]
if not os.path.isfile(path):
print(f"Error: config file not found: {path}", file=sys.stderr)
sys.exit(1)
with open(path, 'r', encoding='utf-8') as f:
cfg = json.load(f)
tenants = cfg.get('tenants')
if tenants is None or not isinstance(tenants, dict):
tenants = {}
for tid, conf in tenants.items():
if isinstance(conf, dict) and str(conf.get('port')) == port_str and tid != tenant_id:
print(f"Error: port {port_str} is already mapped to tenant {tid}", file=sys.stderr)
sys.exit(2)
existing = tenants.get(tenant_id)
if existing is None or not isinstance(existing, dict):
existing = {}
existing['port'] = int(port_str)
tenants[tenant_id] = existing
cfg['tenants'] = tenants
with open(path, 'w', encoding='utf-8') as f:
json.dump(cfg, f, indent=4, ensure_ascii=False)
print(f"Registered tenant port {port_str} for {tenant_id}")
PY
then
echo "Tenant $tenant_id port $port registered in config.json"
else
echo "Failed to register tenant port $port for $tenant_id"
exit 1
fi
}
update_runtime_ports() {
local new_port="$1"
local env_file="$PWD/.docker-build.env"
local current_ports=""
local port_list=()
local unique_ports=()
local first_port=""
if [ -n "$new_port" ]; then
if [ -f "$env_file" ]; then
current_ports="$(awk -F= '/^INVENTAR_HTTP_PORTS=/{print $2; exit}' "$env_file" | tr -d ' ' || true)"
if [ -z "$current_ports" ]; then
current_ports="$(awk -F= '/^INVENTAR_HTTP_PORT=/{print $2; exit}' "$env_file" | tr -d ' ' || true)"
fi
fi
if [ -n "$current_ports" ]; then
IFS=',' read -r -a port_list <<<"${current_ports// /,}"
fi
port_list+=("$new_port")
for port in "${port_list[@]}"; do
if [ -n "$port" ] && ! printf '%s\n' "${unique_ports[@]}" | grep -qx "$port"; then
unique_ports+=("$port")
fi
done
if [ ${#unique_ports[@]} -eq 0 ]; then
unique_ports=("$new_port")
fi
first_port="${unique_ports[0]}"
local ports_csv
ports_csv="$(IFS=,; echo "${unique_ports[*]}")"
if [ ! -f "$env_file" ]; then
cat > "$env_file" <<EOF
NUITKA_BUILD=0
INVENTAR_HTTP_PORT=$first_port
INVENTAR_HTTP_PORTS=$ports_csv
EOF
else
if grep -q '^INVENTAR_HTTP_PORTS=' "$env_file" 2>/dev/null; then
sed -i "s|^INVENTAR_HTTP_PORTS=.*|INVENTAR_HTTP_PORTS=$ports_csv|" "$env_file"
else
printf '\nINVENTAR_HTTP_PORTS=%s\n' "$ports_csv" >> "$env_file"
fi
if grep -q '^INVENTAR_HTTP_PORT=' "$env_file" 2>/dev/null; then
sed -i "s|^INVENTAR_HTTP_PORT=.*|INVENTAR_HTTP_PORT=$first_port|" "$env_file"
else
printf '\nINVENTAR_HTTP_PORT=%s\n' "$first_port" >> "$env_file"
fi
fi
echo "Updated runtime env ports: $ports_csv"
fi
}
if [ -z "${1:-}" ]; then
show_help show_help
fi fi
COMMAND=$1 COMMAND="$1"
TENANT_ID=$2 TENANT_ID="${2:-}"
case "$COMMAND" in case "$COMMAND" in
-h|--help)
show_help
;;
add) add)
if [ -z "$TENANT_ID" ]; then if [ -z "$TENANT_ID" ]; then
echo "Error: Please provide a tenant_id." echo "Error: Please provide a tenant_id."
exit 1 exit 1
fi fi
echo "Adding new tenant '$TENANT_ID'..."
# Add Nginx configuration PORT_ARG="$3"
if [ -f "docker/nginx/multitenant.conf" ]; then if [ -n "$PORT_ARG" ]; then
echo "Assuming dynamic routing based on subdomain ($TENANT_ID)..." if ! printf '%s\n' "$PORT_ARG" | grep -qE '^[0-9]+$'; then
echo "Error: Port must be a numeric value."
exit 1
fi
register_tenant_port "$TENANT_ID" "$PORT_ARG"
update_runtime_ports "$PORT_ARG"
fi fi
echo "Adding new tenant '$TENANT_ID'..."
# Initialize tenant database via Python inside container # Initialize tenant database via Python inside container
echo "Initializing database for $TENANT_ID..." echo "Initializing database for $TENANT_ID..."
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1) APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
@@ -118,17 +227,46 @@ print(f'Tenant {sys.argv[1]} session cache cleared. Tenant restarted.')
;; ;;
list) list)
echo "Listing active tenants (Databases):" echo "Listing configured tenants (config.json):"
if [ -f "$CONFIG_FILE" ]; then
python3 - "$CONFIG_FILE" <<'PY'
import json, sys
path = sys.argv[1]
with open(path, 'r', encoding='utf-8') as f:
cfg = json.load(f)
tenants = cfg.get('tenants', {})
if isinstance(tenants, dict) and tenants:
for tid, conf in tenants.items():
port = conf.get('port') if isinstance(conf, dict) else None
if port is not None:
print(f'- {tid} (port {port})')
else:
print(f'- {tid}')
else:
print(' (no tenants configured)')
PY
else
echo " (config.json not found)"
fi
echo ""
echo "Listing active tenant databases (MongoDB):"
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1) APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
if [ -n "$APP_CONTAINER" ]; then if [ -n "$APP_CONTAINER" ]; then
docker exec $APP_CONTAINER python3 -c " docker exec -i "$APP_CONTAINER" python3 - <<'PY'
import sys; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient import sys
sys.path.insert(0, '/app/Web')
import settings
from pymongo import MongoClient
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT)) client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
prefix = f'{settings.MONGODB_DB}_' prefix = f'{settings.MONGODB_DB}_'
dbs = [d for d in client.list_database_names() if d.startswith(prefix)] dbs = [d for d in client.list_database_names() if d.startswith(prefix)]
for db in dbs: if dbs:
print(f'- {db.replace(prefix, \"\")}') for db in dbs:
" print(f'- {db.replace(prefix, "")}')
else:
print(' (no tenant databases found)')
PY
else else
echo "Error: Application container not running." echo "Error: Application container not running."
fi fi
+51 -11
View File
@@ -1,14 +1,54 @@
#!/bin/bash #!/usr/bin/env 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 set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
cd "$SCRIPT_DIR"
"$SCRIPT_DIR/stop.sh" "$@" COMPOSE_FILE="docker-compose-multitenant.yml"
"$SCRIPT_DIR/start.sh" "$@" ENV_FILE="$SCRIPT_DIR/.docker-build.env"
RUNTIME_COMPOSE_OVERRIDE_FILE="$SCRIPT_DIR/.docker-compose.runtime.override.yml"
while [[ $# -gt 0 ]]; do
case "$1" in
--multitenant)
COMPOSE_FILE="docker-compose-multitenant.yml"
shift
;;
--singletenant)
COMPOSE_FILE="docker-compose.yml"
shift
;;
*)
shift
;;
esac
done
if ! command -v docker >/dev/null 2>&1; then
echo "Error: docker command not found. Install Docker first."
exit 1
fi
echo "Rebuilding and/or restarting app container using $COMPOSE_FILE..."
compose_args=(-f "$COMPOSE_FILE")
if [ -f "$RUNTIME_COMPOSE_OVERRIDE_FILE" ]; then
compose_args+=( -f "$RUNTIME_COMPOSE_OVERRIDE_FILE" )
fi
if [ -f "$ENV_FILE" ]; then
compose_args+=( --env-file "$ENV_FILE" )
fi
if [ -f "$SCRIPT_DIR/Dockerfile" ]; then
docker compose "${compose_args[@]}" up -d --build app
else
echo "Warning: Dockerfile not found in $SCRIPT_DIR. Skipping build and restarting existing app container."
if ! docker compose "${compose_args[@]}" up -d --no-build app; then
echo "Warning: app image not found or not available locally. Attempting docker compose pull app..."
docker compose "${compose_args[@]}" pull app
docker compose "${compose_args[@]}" up -d --no-build app
fi
fi
echo "Cleaning up unused Docker images..."
docker image prune -f
echo "App restart complete."
+118 -196
View File
@@ -14,9 +14,15 @@ if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo" SUDO="sudo"
fi fi
IS_ROOT="false"
if [ "$(id -u)" -eq 0 ]; then
IS_ROOT="true"
fi
NUITKA_BUILD_VALUE="0" NUITKA_BUILD_VALUE="0"
HTTP_PORT_VALUE="8001" HTTP_PORT_VALUE="10000"
HTTPS_PORT_VALUE="8443" HTTP_PORTS_VALUE=""
DEFAULT_TENANT_PORT_START="${INVENTAR_TENANT_PORT_START:-10000}"
CRON_SETUP_VALUE="${INVENTAR_SETUP_CRON:-1}" CRON_SETUP_VALUE="${INVENTAR_SETUP_CRON:-1}"
APP_IMAGE_VALUE="${INVENTAR_APP_IMAGE:-$APP_IMAGE_REPO:latest}" APP_IMAGE_VALUE="${INVENTAR_APP_IMAGE:-$APP_IMAGE_REPO:latest}"
COMPOSE_FILE="docker-compose-multitenant.yml" COMPOSE_FILE="docker-compose-multitenant.yml"
@@ -114,7 +120,11 @@ ensure_runtime_dependencies() {
local missing=() local missing=()
if ! command -v docker >/dev/null 2>&1; then if ! command -v docker >/dev/null 2>&1; then
install_docker_engine if [ "$IS_ROOT" = "true" ]; then
install_docker_engine
else
missing+=(docker)
fi
fi fi
if ! docker compose version >/dev/null 2>&1; then if ! docker compose version >/dev/null 2>&1; then
@@ -138,14 +148,20 @@ ensure_runtime_dependencies() {
fi fi
if [ "${#missing[@]}" -gt 0 ]; then if [ "${#missing[@]}" -gt 0 ]; then
echo "Installing missing dependencies: ${missing[*]}" if [ "$IS_ROOT" = "true" ]; then
apt_install "${missing[@]}" echo "Installing missing dependencies: ${missing[*]}"
apt_install "${missing[@]}"
else
echo "ERROR: Missing dependencies: ${missing[*]}"
echo "Please install the missing tools or run this script as root."
exit 1
fi
fi fi
if command -v systemctl >/dev/null 2>&1; then if [ "$IS_ROOT" = "true" ] && command -v systemctl >/dev/null 2>&1; then
$SUDO systemctl enable --now docker >/dev/null 2>&1 || true systemctl enable --now docker >/dev/null 2>&1 || true
if cron_setup_enabled; then if cron_setup_enabled; then
$SUDO systemctl enable --now cron >/dev/null 2>&1 || true systemctl enable --now cron >/dev/null 2>&1 || true
fi fi
fi fi
} }
@@ -158,6 +174,11 @@ setup_boot_autostart_service() {
return 0 return 0
fi fi
if [ "$IS_ROOT" != "true" ]; then
echo "Skipping systemd autostart setup when not running as root."
return 0
fi
if ! command -v systemctl >/dev/null 2>&1; then if ! command -v systemctl >/dev/null 2>&1; then
return 0 return 0
fi fi
@@ -199,6 +220,11 @@ setup_scheduled_jobs() {
return 0 return 0
fi fi
if [ "$IS_ROOT" != "true" ]; then
echo "Skipping cron job setup when not running as root."
return 0
fi
if ! command -v crontab >/dev/null 2>&1; then if ! command -v crontab >/dev/null 2>&1; then
echo "Warning: crontab not available, skipping nightly update setup" echo "Warning: crontab not available, skipping nightly update setup"
return 0 return 0
@@ -209,101 +235,17 @@ setup_scheduled_jobs() {
backup_line="30 2 * * * cd $SCRIPT_DIR && ./backup.sh --mode auto >> $SCRIPT_DIR/logs/backup.log 2>&1" backup_line="30 2 * * * cd $SCRIPT_DIR && ./backup.sh --mode auto >> $SCRIPT_DIR/logs/backup.log 2>&1"
local existing_cron local existing_cron
if [ "$(id -u)" -eq 0 ]; then existing_cron="$(crontab -l 2>/dev/null || true)"
existing_cron="$(crontab -l 2>/dev/null || true)" {
{ printf '%s\n' "$existing_cron" | grep -vF "$SCRIPT_DIR/update.sh" | grep -vF "$SCRIPT_DIR/backup-docker.sh" | grep -vF "$SCRIPT_DIR/backup.sh" || true
printf '%s\n' "$existing_cron" | grep -vF "$SCRIPT_DIR/update.sh" | grep -vF "$SCRIPT_DIR/backup-docker.sh" | grep -vF "$SCRIPT_DIR/backup.sh" || true echo "$backup_line"
echo "$backup_line" echo "$update_line"
echo "$update_line" } | crontab -
} | crontab -
else
existing_cron="$($SUDO crontab -l 2>/dev/null || true)"
{
printf '%s\n' "$existing_cron" | grep -vF "$SCRIPT_DIR/update.sh" | grep -vF "$SCRIPT_DIR/backup-docker.sh" | grep -vF "$SCRIPT_DIR/backup.sh" || true
echo "$backup_line"
echo "$update_line"
} | $SUDO crontab -
fi
echo "Nightly backup scheduled at 02:30" echo "Nightly backup scheduled at 02:30"
echo "Nightly auto-update scheduled at 03:00" echo "Nightly auto-update scheduled at 03:00"
} }
ensure_tls_certificates() {
local cert_dir cert_path key_path cn
cert_dir="$SCRIPT_DIR/certs"
cert_path="$cert_dir/inventarsystem.crt"
key_path="$cert_dir/inventarsystem.key"
mkdir -p "$cert_dir"
if [ -f "$cert_path" ] && [ -f "$key_path" ]; then
return 0
fi
cn="${TLS_CN:-localhost}"
echo "No TLS certificates found. Generating self-signed certificate for CN=$cn"
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout "$key_path" \
-out "$cert_path" \
-subj "/C=DE/ST=NA/L=NA/O=Inventarsystem/OU=IT/CN=$cn" >/dev/null 2>&1
chmod 600 "$key_path"
chmod 644 "$cert_path"
}
ensure_nginx_config_mount_source() {
local nginx_dir config_path backup_path
nginx_dir="$SCRIPT_DIR/docker/nginx"
config_path="$nginx_dir/default.conf"
mkdir -p "$nginx_dir"
if [ -d "$config_path" ]; then
backup_path="${config_path}.dir.$(date +%Y%m%d-%H%M%S).bak"
mv "$config_path" "$backup_path"
echo "Warning: moved unexpected directory $config_path to $backup_path"
fi
if [ ! -f "$config_path" ]; then
cat > "$config_path" <<'EOF'
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/nginx/certs/inventarsystem.crt;
ssl_certificate_key /etc/nginx/certs/inventarsystem.key;
client_max_body_size 50M;
location / {
proxy_pass http://app:8000;
proxy_http_version 1.1;
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;
proxy_read_timeout 300;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
default_type text/html;
return 200 '<!doctype html><html><head><meta charset="utf-8"><title>Server Error</title></head><body><h1>Server Error</h1><p>The service is temporarily unavailable.</p></body></html>';
}
}
EOF
echo "Recreated missing nginx config at $config_path"
fi
}
ensure_runtime_config_json() { ensure_runtime_config_json() {
local config_path backup_path local config_path backup_path
config_path="$SCRIPT_DIR/config.json" config_path="$SCRIPT_DIR/config.json"
@@ -320,7 +262,7 @@ ensure_runtime_config_json() {
"ver": "2.6.5", "ver": "2.6.5",
"dbg": false, "dbg": false,
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 443, "port": 8000,
"mongodb": { "mongodb": {
"host": "mongodb", "host": "mongodb",
"port": 27017, "port": 27017,
@@ -441,16 +383,6 @@ resolve_app_image() {
fi fi
} }
configure_cloudflared_profile() {
if [ -f /etc/cloudflared/credentials.json ]; then
COMPOSE_PROFILES_VALUE="cloudflare"
echo "Cloudflared tunnel: enabled (homeserver)"
else
COMPOSE_PROFILES_VALUE=""
echo "Cloudflared tunnel: disabled (missing /etc/cloudflared/credentials.json)"
fi
}
port_in_use() { port_in_use() {
local port="$1" local port="$1"
@@ -469,97 +401,75 @@ find_free_port() {
echo "$port" echo "$port"
} }
stack_owns_host_port() { parse_port_list() {
local requested_port="$1" local raw="$1"
local container_port="$2" local port
local mapped_port local ports=()
raw="${raw//,/ }"
mapped_port="$(docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" port nginx "$container_port" 2>/dev/null | tail -n1 || true)" for port in $raw; do
if [ -z "$mapped_port" ]; then port="${port//[[:space:]]/}"
return 1 if [ -n "$port" ] && printf '%s\n' "$port" | grep -qE '^[0-9]+$'; then
fi ports+=("$port")
fi
mapped_port="${mapped_port##*:}" done
[ "$mapped_port" = "$requested_port" ] printf '%s\n' "${ports[@]}"
}
stop_host_nginx_services() {
local stopped_any=false
local service_name
if ! command -v systemctl >/dev/null 2>&1; then
return 1
fi
while IFS= read -r service_name; do
[ -z "$service_name" ] && continue
echo "Stopping host service $service_name to free web ports..."
$SUDO systemctl stop "$service_name" >/dev/null 2>&1 || true
stopped_any=true
done < <(systemctl list-units --type=service --state=active --no-pager 2>/dev/null | awk '{print $1}' | grep -E '(^nginx\.service$|nginx)' || true)
if [ "$stopped_any" = true ]; then
sleep 2
fi
if [ "$stopped_any" = true ]; then
return 0
fi
return 1
} }
configure_host_ports() { configure_host_ports() {
local requested_http requested_https local requested_http
local requested_ports
local ports=()
local occupied_ports=()
requested_http="" requested_http=""
requested_ports=""
if [ -f "$ENV_FILE" ]; then if [ -f "$ENV_FILE" ]; then
requested_http="$(awk -F= '/^INVENTAR_HTTP_PORT=/{print $2}' "$ENV_FILE" | tr -d ' ' || true)" requested_http="$(awk -F= '/^INVENTAR_HTTP_PORT=/{print $2}' "$ENV_FILE" | tr -d ' ' || true)"
fi requested_ports="$(awk -F= '/^INVENTAR_HTTP_PORTS=/{print $2}' "$ENV_FILE" | tr -d ' ' || true)"
if [ -z "$requested_http" ]; then
requested_http="8001"
fi fi
if stack_owns_host_port "$requested_http" "80"; then if [ -n "${INVENTAR_HTTP_PORTS:-}" ]; then
HTTP_PORT_VALUE="$requested_http" requested_ports="$INVENTAR_HTTP_PORTS"
elif port_in_use "$requested_http"; then fi
if ! port_in_use "$requested_http"; then if [ -n "${INVENTAR_HTTP_PORT:-}" ] && [ -z "$requested_ports" ]; then
HTTP_PORT_VALUE="$requested_http" requested_ports="$INVENTAR_HTTP_PORT"
echo "Freed HTTP port $requested_http by stopping host nginx service" fi
else
HTTP_PORT_VALUE="$(find_free_port 8080)" if [ -n "$requested_ports" ]; then
echo "HTTP port is in use. Using fallback HTTP port: $HTTP_PORT_VALUE" mapfile -t ports < <(parse_port_list "$requested_ports")
fi
if [ ${#ports[@]} -gt 0 ]; then
HTTP_PORTS_VALUE="${ports[*]}"
HTTP_PORT_VALUE="${ports[0]}"
for port in "${ports[@]}"; do
if port_in_use "$port"; then
occupied_ports+=("$port")
fi
done
if [ ${#occupied_ports[@]} -gt 0 ]; then
if [ ${#ports[@]} -eq 1 ]; then
HTTP_PORT_VALUE="$(find_free_port "$DEFAULT_TENANT_PORT_START")"
echo "Host port ${ports[0]} is already occupied. Assigned new tenant port: $HTTP_PORT_VALUE"
HTTP_PORTS_VALUE="$HTTP_PORT_VALUE"
else
echo "Error: requested host port(s) already in use: ${occupied_ports[*]}"
echo "Remove the occupied port(s) from INVENTAR_HTTP_PORTS or stop the conflicting service."
exit 1
fi
fi fi
else else
HTTP_PORT_VALUE="$requested_http" HTTP_PORT_VALUE="$DEFAULT_TENANT_PORT_START"
fi HTTP_PORTS_VALUE="$HTTP_PORT_VALUE"
requested_https="" if port_in_use "$HTTP_PORT_VALUE"; then
if [ -f "$ENV_FILE" ]; then HTTP_PORT_VALUE="$(find_free_port "$DEFAULT_TENANT_PORT_START")"
requested_https="$(awk -F= '/^INVENTAR_HTTPS_PORT=/{print $2}' "$ENV_FILE" | tr -d ' ' || true)" echo "Host port $DEFAULT_TENANT_PORT_START is already occupied. Assigned new tenant port: $HTTP_PORT_VALUE"
fi HTTP_PORTS_VALUE="$HTTP_PORT_VALUE"
if [ -z "$requested_https" ]; then
requested_https="8443"
fi
if stack_owns_host_port "$requested_https" "443"; then
HTTPS_PORT_VALUE="$requested_https"
elif port_in_use "$requested_https"; then
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 fi
HTTPS_PORT_VALUE="$(find_free_port 8443)"
echo "HTTPS port is in use. Using fallback HTTPS port: $HTTPS_PORT_VALUE"
else
HTTPS_PORT_VALUE="$requested_https"
fi fi
} }
@@ -598,7 +508,7 @@ write_env_file() {
cat > "$ENV_FILE" <<EOF cat > "$ENV_FILE" <<EOF
NUITKA_BUILD=$NUITKA_BUILD_VALUE NUITKA_BUILD=$NUITKA_BUILD_VALUE
INVENTAR_HTTP_PORT=$HTTP_PORT_VALUE INVENTAR_HTTP_PORT=$HTTP_PORT_VALUE
INVENTAR_HTTPS_PORT=$HTTPS_PORT_VALUE INVENTAR_HTTP_PORTS=${HTTP_PORTS_VALUE// /,}
INVENTAR_APP_IMAGE=$APP_IMAGE_VALUE INVENTAR_APP_IMAGE=$APP_IMAGE_VALUE
EOF EOF
} }
@@ -612,6 +522,21 @@ services:
image: ${APP_IMAGE_VALUE} image: ${APP_IMAGE_VALUE}
build: null build: null
EOF EOF
if [ -n "$HTTP_PORTS_VALUE" ]; then
local ports_array
read -r -a ports_array <<<"$HTTP_PORTS_VALUE"
if [ "${#ports_array[@]}" -gt 1 ]; then
cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <<EOF
ports:
EOF
for port in "${ports_array[@]}"; do
cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <<EOF
- "$port:8000"
EOF
done
fi
fi
} }
verify_stack_health() { verify_stack_health() {
@@ -628,10 +553,10 @@ verify_stack_health() {
for _ in $(seq 1 60); do for _ in $(seq 1 60); do
running_services="$(docker compose "${compose_args[@]}" ps --status running --services 2>/dev/null || true)" running_services="$(docker compose "${compose_args[@]}" ps --status running --services 2>/dev/null || true)"
if printf '%s\n' "$running_services" | grep -Fxq app && \ if printf '%s\n' "$running_services" | grep -Fxq app && \
printf '%s\n' "$running_services" | grep -Fxq nginx && \ printf '%s\n' "$running_services" | grep -Fxq redis && \
printf '%s\n' "$running_services" | grep -Fxq mongodb; then 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 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 -fsS "http://127.0.0.1:$HTTP_PORT_VALUE/health" >/dev/null 2>&1; then
echo "Health check passed." echo "Health check passed."
return 0 return 0
fi fi
@@ -644,8 +569,8 @@ verify_stack_health() {
if [[ $retry_count -eq 0 ]]; then if [[ $retry_count -eq 0 ]]; then
echo "Health check failed. Attempting to restart containers..." echo "Health check failed. Attempting to restart containers..."
docker compose "${compose_args[@]}" ps || true docker compose "${compose_args[@]}" ps || true
docker compose "${compose_args[@]}" logs --tail=120 app nginx mongodb || true docker compose "${compose_args[@]}" logs --tail=120 app redis mongodb || true
docker compose "${compose_args[@]}" restart app nginx mongodb docker compose "${compose_args[@]}" restart app redis mongodb
sleep 3 sleep 3
((retry_count++)) ((retry_count++))
else else
@@ -656,7 +581,7 @@ verify_stack_health() {
# Final failure # Final failure
echo "Error: stack health check failed after restart attempt." echo "Error: stack health check failed after restart attempt."
docker compose "${compose_args[@]}" ps || true docker compose "${compose_args[@]}" ps || true
docker compose "${compose_args[@]}" logs --tail=120 app nginx mongodb || true docker compose "${compose_args[@]}" logs --tail=120 app redis mongodb || true
return 1 return 1
} }
@@ -664,13 +589,10 @@ parse_args "$@"
ensure_runtime_dependencies ensure_runtime_dependencies
setup_boot_autostart_service setup_boot_autostart_service
ensure_tls_certificates
ensure_nginx_config_mount_source
ensure_runtime_config_json ensure_runtime_config_json
setup_scheduled_jobs setup_scheduled_jobs
configure_nuitka_mode configure_nuitka_mode
resolve_app_image resolve_app_image
configure_cloudflared_profile
configure_host_ports configure_host_ports
ensure_min_docker_disk_space ensure_min_docker_disk_space
ensure_app_image_loaded ensure_app_image_loaded
@@ -695,4 +617,4 @@ fi
verify_stack_health verify_stack_health
echo "Stack started." echo "Stack started."
echo "Open: https://<server-ip>:$HTTPS_PORT_VALUE" echo "Open: http://<server-ip>:$HTTP_PORT_VALUE"
+26 -101
View File
@@ -38,6 +38,11 @@ if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo" SUDO="sudo"
fi fi
IS_ROOT="false"
if [ "$(id -u)" -eq 0 ]; then
IS_ROOT="true"
fi
apt_install() { apt_install() {
$SUDO apt-get update -y $SUDO apt-get update -y
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"
@@ -47,7 +52,7 @@ ensure_runtime_dependencies() {
local missing=() local missing=()
if ! command -v docker >/dev/null 2>&1; then if ! command -v docker >/dev/null 2>&1; then
missing+=(docker.io) missing+=(docker)
fi fi
if ! docker compose version >/dev/null 2>&1; then if ! docker compose version >/dev/null 2>&1; then
@@ -67,63 +72,21 @@ ensure_runtime_dependencies() {
fi fi
if [ "${#missing[@]}" -gt 0 ]; then if [ "${#missing[@]}" -gt 0 ]; then
log_message "Installing missing dependencies: ${missing[*]}" if [ "$IS_ROOT" = "true" ]; then
apt_install "${missing[@]}" log_message "Installing missing dependencies: ${missing[*]}"
apt_install "${missing[@]}"
else
log_message "ERROR: Missing dependencies: ${missing[*]}"
log_message "Install the missing tools manually or re-run as root."
exit 1
fi
fi fi
if command -v systemctl >/dev/null 2>&1; then if [ "$IS_ROOT" = "true" ] && command -v systemctl >/dev/null 2>&1; then
$SUDO systemctl enable --now docker >/dev/null 2>&1 || true systemctl enable --now docker >/dev/null 2>&1 || true
fi fi
} }
ensure_tls_certificates() {
local cert_dir cert_path key_path cn
cert_dir="$PROJECT_DIR/certs"
cert_path="$cert_dir/inventarsystem.crt"
key_path="$cert_dir/inventarsystem.key"
mkdir -p "$cert_dir"
if [ -f "$cert_path" ] && [ -f "$key_path" ]; then
return 0
fi
cn="${TLS_CN:-localhost}"
log_message "No TLS certificates found. Generating self-signed certificate for CN=$cn"
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout "$key_path" \
-out "$cert_path" \
-subj "/C=DE/ST=NA/L=NA/O=Inventarsystem/OU=IT/CN=$cn" >/dev/null 2>&1
chmod 600 "$key_path"
chmod 644 "$cert_path"
}
ensure_nginx_config_mount_source() {
local nginx_dir config_path backup_path
nginx_dir="$PROJECT_DIR/docker/nginx"
config_path="$nginx_dir/default.conf"
mkdir -p "$nginx_dir"
if [ -d "$config_path" ]; then
backup_path="${config_path}.dir.$(date +%Y%m%d-%H%M%S).bak"
mv "$config_path" "$backup_path"
log_message "WARNING: Moved unexpected directory $config_path to $backup_path"
fi
if [ ! -f "$config_path" ]; then
cat > "$config_path" <<'EOF'
fi
}
server() {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
ensure_min_root_disk_space() { ensure_min_root_disk_space() {
local available_kb available_mb local available_kb available_mb
@@ -183,36 +146,6 @@ cleanup_docker_dangling_images() {
fi fi
} }
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/nginx/certs/inventarsystem.crt;
ssl_certificate_key /etc/nginx/certs/inventarsystem.key;
client_max_body_size 50M;
location / {
proxy_pass http://app:8000;
proxy_http_version 1.1;
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;
proxy_read_timeout 300;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
default_type text/html;
return 200 '<!doctype html><html><head><meta charset="utf-8"><title>Server Error</title></head><body><h1>Server Error</h1><p>The service is temporarily unavailable.</p></body></html>';
}
}
EOF
log_message "Recreated missing nginx config at $config_path"
fi
}
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $0 [options] Usage: $0 [options]
@@ -411,9 +344,7 @@ download_and_extract_bundle() {
tar -xzf "$archive" -C "$tmp_dir" tar -xzf "$archive" -C "$tmp_dir"
# The bundle must contain docker deployment files only. # The bundle must contain docker deployment files only.
mkdir -p "$PROJECT_DIR/docker/nginx"
cp -f "$tmp_dir/docker-compose.yml" "$PROJECT_DIR/docker-compose.yml" cp -f "$tmp_dir/docker-compose.yml" "$PROJECT_DIR/docker-compose.yml"
cp -f "$tmp_dir/docker/nginx/default.conf" "$PROJECT_DIR/docker/nginx/default.conf"
cp -f "$tmp_dir/start.sh" "$PROJECT_DIR/start.sh" cp -f "$tmp_dir/start.sh" "$PROJECT_DIR/start.sh"
cp -f "$tmp_dir/stop.sh" "$PROJECT_DIR/stop.sh" cp -f "$tmp_dir/stop.sh" "$PROJECT_DIR/stop.sh"
@@ -429,9 +360,6 @@ download_and_extract_bundle() {
if [ -f "$tmp_dir/docker-compose-multitenant.yml" ]; then if [ -f "$tmp_dir/docker-compose-multitenant.yml" ]; then
cp -f "$tmp_dir/docker-compose-multitenant.yml" "$PROJECT_DIR/docker-compose-multitenant.yml" cp -f "$tmp_dir/docker-compose-multitenant.yml" "$PROJECT_DIR/docker-compose-multitenant.yml"
fi fi
if [ -f "$tmp_dir/docker/nginx/multitenant.conf" ]; then
cp -f "$tmp_dir/docker/nginx/multitenant.conf" "$PROJECT_DIR/docker/nginx/multitenant.conf"
fi
if [ -f "$tmp_dir/manage-tenant.sh" ]; then if [ -f "$tmp_dir/manage-tenant.sh" ]; then
cp -f "$tmp_dir/manage-tenant.sh" "$PROJECT_DIR/manage-tenant.sh" cp -f "$tmp_dir/manage-tenant.sh" "$PROJECT_DIR/manage-tenant.sh"
fi fi
@@ -472,8 +400,7 @@ deploy() {
if [ ! -f "$ENV_FILE" ]; then if [ ! -f "$ENV_FILE" ]; then
cat > "$ENV_FILE" <<EOF cat > "$ENV_FILE" <<EOF
NUITKA_BUILD=0 NUITKA_BUILD=0
INVENTAR_HTTP_PORT=80 INVENTAR_HTTP_PORT=10000
INVENTAR_HTTPS_PORT=443
INVENTAR_APP_IMAGE=$app_image INVENTAR_APP_IMAGE=$app_image
EOF EOF
elif grep -q '^INVENTAR_APP_IMAGE=' "$ENV_FILE"; then elif grep -q '^INVENTAR_APP_IMAGE=' "$ENV_FILE"; then
@@ -492,27 +419,27 @@ EOF
fi fi
fi fi
docker compose -f "$compose_path" --env-file "$ENV_FILE" pull nginx mongodb >> "$LOG_FILE" 2>&1 docker compose -f "$compose_path" --env-file "$ENV_FILE" pull app mongodb >> "$LOG_FILE" 2>&1
docker compose -f "$compose_path" --env-file "$ENV_FILE" up -d --remove-orphans >> "$LOG_FILE" 2>&1 docker compose -f "$compose_path" --env-file "$ENV_FILE" up -d --remove-orphans >> "$LOG_FILE" 2>&1
docker tag "$app_image" "$APP_IMAGE_REPO:latest" >> "$LOG_FILE" 2>&1 || true docker tag "$app_image" "$APP_IMAGE_REPO:latest" >> "$LOG_FILE" 2>&1 || true
} }
verify_stack_health() { verify_stack_health() {
local compose_args running_services local compose_args running_services
local https_port local http_port
compose_args=(-f "$PROJECT_DIR/$COMPOSE_FILE" --env-file "$ENV_FILE") compose_args=(-f "$PROJECT_DIR/$COMPOSE_FILE" --env-file "$ENV_FILE")
https_port="$(awk -F= '/^INVENTAR_HTTPS_PORT=/{print $2}' "$ENV_FILE" | tr -d ' ')" http_port="$(awk -F= '/^INVENTAR_HTTP_PORT=/{print $2}' "$ENV_FILE" | tr -d ' ')"
if [ -z "$https_port" ]; then if [ -z "$http_port" ]; then
https_port="443" http_port="10000"
fi fi
for _ in $(seq 1 60); do for _ in $(seq 1 60); do
running_services="$(docker compose "${compose_args[@]}" ps --status running --services 2>/dev/null || true)" running_services="$(docker compose "${compose_args[@]}" ps --status running --services 2>/dev/null || true)"
if printf '%s\n' "$running_services" | grep -Fxq app && \ if printf '%s\n' "$running_services" | grep -Fxq app && \
printf '%s\n' "$running_services" | grep -Fxq nginx && \ printf '%s\n' "$running_services" | grep -Fxq redis && \
printf '%s\n' "$running_services" | grep -Fxq mongodb; then printf '%s\n' "$running_services" | grep -Fxq mongodb; then
# Primary check: HTTP endpoint responds (most reliable) # Primary check: health endpoint responds (most reliable)
if curl -kfsS "https://127.0.0.1:$https_port" >/dev/null 2>&1; then if curl -fsS "http://127.0.0.1:$http_port/health" >/dev/null 2>&1; then
return 0 return 0
fi fi
fi fi
@@ -520,7 +447,7 @@ verify_stack_health() {
done done
docker compose "${compose_args[@]}" ps >> "$LOG_FILE" 2>&1 || true docker compose "${compose_args[@]}" ps >> "$LOG_FILE" 2>&1 || true
docker compose "${compose_args[@]}" logs --tail=120 app nginx mongodb >> "$LOG_FILE" 2>&1 || true docker compose "${compose_args[@]}" logs --tail=120 app redis mongodb >> "$LOG_FILE" 2>&1 || true
return 1 return 1
} }
@@ -548,8 +475,6 @@ main() {
cleanup_server_space cleanup_server_space
ensure_runtime_dependencies ensure_runtime_dependencies
ensure_tls_certificates
ensure_nginx_config_mount_source
require_cmd curl require_cmd curl
require_cmd tar require_cmd tar