Compare commits

...

11 Commits

Author SHA1 Message Date
Aiirondev_dev 4a18460178 Enhance tenant management script: add functionality to remove tenant ports and update runtime port configuration 2026-04-28 19:00:08 +02:00
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
9 changed files with 555 additions and 398 deletions
+1 -2
View File
@@ -1,4 +1,3 @@
NUITKA_BUILD=0
INVENTAR_HTTP_PORT=80
INVENTAR_HTTPS_PORT=8000
INVENTAR_HTTP_PORT=10000
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
run: |
mkdir -p release-bundle
mkdir -p release-bundle/docker/nginx
cat > release-bundle/docker-compose.yml <<EOF
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:
image: ${INVENTAR_APP_IMAGE:-ghcr.io/aiirondev/legendary-octo-garbanzo:latest}
pull_policy: never
image: ${INVENTAR_APP_IMAGE:-ghcr.io/aiirondev/legendary-octo-garbanzo:${{ steps.meta.outputs.tag }}}
container_name: inventarsystem-app
restart: unless-stopped
ports:
- "${INVENTAR_HTTP_PORT:-10000}:8000"
depends_on:
mongodb:
condition: service_healthy
- mongodb
- redis
environment:
INVENTAR_MONGODB_HOST: mongodb
INVENTAR_MONGODB_PORT: "27017"
@@ -211,6 +186,33 @@ jobs:
- app_backups:/data/backups
- 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:
mongodb_data:
app_uploads:
@@ -219,9 +221,9 @@ jobs:
app_qrcodes:
app_backups:
app_logs:
redis_data:
EOF
cp docker/nginx/default.conf release-bundle/docker/nginx/default.conf
cp start.sh release-bundle/start.sh
cp stop.sh release-bundle/stop.sh
cp restart.sh release-bundle/restart.sh
@@ -232,7 +234,6 @@ jobs:
# Multitenant scripts & docs
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 run-tenant-cmd.sh release-bundle/run-tenant-cmd.sh
cp MULTITENANT_DEPLOYMENT.md release-bundle/MULTITENANT_DEPLOYMENT.md
+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 functools import wraps
import logging
import os
import re
import settings as cfg
logger = logging.getLogger(__name__)
@@ -30,6 +32,46 @@ def _get_nested_value(source, path, default=None):
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):
"""Return the registered config for a tenant, falling back to default."""
if tenant_id is None:
@@ -52,19 +94,20 @@ def is_tenant_module_enabled(module_name, tenant_id=None, default=False):
class TenantContext:
"""
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):
self.tenant_id = None
self.db_name = None
self.subdomain = None
self.port = None
self.config = {}
def resolve_tenant(self):
"""
Resolve tenant from request context.
Priority: Header > Subdomain > Default
Priority: Header > Port mapping > Subdomain > Default
"""
if not has_request_context():
return None
@@ -76,8 +119,18 @@ class TenantContext:
self.config = get_tenant_config(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()
_, 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('.')
# Extract subdomain from host
@@ -93,7 +146,7 @@ class TenantContext:
self.config = get_tenant_config(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.config = get_tenant_config('default')
return self._get_db_name('default')
+5 -44
View File
@@ -1,28 +1,14 @@
# 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
#
# Usage:
# docker-compose -f docker-compose-multitenant.yml up -d
#
# Scale example: To support 10 tenants with 20 users each:
# docker-compose -f docker-compose-multitenant.yml up -d --scale app=10
# Example: Start the stack and expose the app on host port 10000
# INVENTAR_HTTP_PORT=10000 docker-compose -f docker-compose-multitenant.yml up -d
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
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
depends_on:
nginx:
condition: service_started
networks:
- inventar-net
# Management Container for multi-tenant scripts
tenant-manager:
image: docker:cli
@@ -35,33 +21,6 @@ services:
- .:/workspace
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:-10000}:80"
- "${INVENTAR_HTTPS_PORT:-10001}: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:
image: redis:7-alpine
container_name: inventarsystem-redis
@@ -115,6 +74,8 @@ services:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "${INVENTAR_HTTP_PORT:-10000}:8000"
networks:
- inventar-net
expose:
+263 -23
View File
@@ -1,4 +1,6 @@
#!/bin/sh
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# Script to manage multitenant deployment
# Allows adding, removing, and restarting tenants without downtime for others
@@ -7,42 +9,239 @@ if [ ! -f "docker-compose-multitenant.yml" ]; then
exit 1
fi
CONFIG_FILE="$PWD/config.json"
show_help() {
echo "Usage: ./manage-tenant.sh [COMMAND] [OPTIONS]"
echo ""
echo "Commands:"
echo " add <tenant_id> Add a new tenant (initializes database)"
echo " remove <tenant_id> Remove a tenant completely (deletes data!)"
echo " restart-tenant <id> 'Restart' a single tenant (clears cache/sessions)"
echo " restart-all Restart all application containers (zero-downtime reload)"
echo " list List active tenants"
echo " add <tenant_id> [port] Add a new tenant (initializes database)"
echo " remove <tenant_id> Remove a tenant completely (deletes data!)"
echo " restart-tenant <id> 'Restart' a single tenant (clears cache/sessions)"
echo " restart-all Restart all application containers (zero-downtime reload)"
echo " list List active tenants"
echo " -h, --help Show this help message"
echo ""
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 restart-all"
echo " ./manage-tenant.sh -h"
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
}
remove_tenant_port() {
local tenant_id="$1"
local config_path="$CONFIG_FILE"
local port
port="$(python3 - "$config_path" "$tenant_id" <<'PY'
import json, sys, os
path, tenant_id = sys.argv[1], sys.argv[2]
if not os.path.isfile(path):
sys.exit(1)
with open(path, 'r', encoding='utf-8') as f:
cfg = json.load(f)
tenants = cfg.get('tenants', {})
if not isinstance(tenants, dict) or tenant_id not in tenants or not isinstance(tenants[tenant_id], dict):
sys.exit(2)
removed = tenants.pop(tenant_id, None)
cfg['tenants'] = tenants
with open(path, 'w', encoding='utf-8') as f:
json.dump(cfg, f, indent=4, ensure_ascii=False)
if removed is not None:
port = removed.get('port')
if port is not None:
print(port)
PY
)"
local status=$?
if [ $status -eq 1 ]; then
echo "Error: config file not found: $config_path"
return 1
fi
if [ $status -eq 2 ]; then
return 2
fi
if [ -n "$port" ]; then
echo "$port"
fi
return 0
}
remove_runtime_port() {
local removed_port="$1"
local env_file="$PWD/.docker-build.env"
local current_ports=""
local port_list=()
local new_ports=()
local first_port=""
if [ ! -f "$env_file" ]; then
return 0
fi
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
if [ -z "$current_ports" ]; then
return 0
fi
IFS=',' read -r -a port_list <<<"${current_ports// /,}"
for port in "${port_list[@]}"; do
if [ -n "$port" ] && [ "$port" != "$removed_port" ]; then
new_ports+=("$port")
fi
done
if [ ${#new_ports[@]} -eq 0 ]; then
sed -i '/^INVENTAR_HTTP_PORTS=/d;/^INVENTAR_HTTP_PORT=/d' "$env_file"
return 0
fi
first_port="${new_ports[0]}"
local ports_csv="$(IFS=,; echo "${new_ports[*]}")"
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
}
if [ -z "${1:-}" ]; then
show_help
fi
COMMAND=$1
TENANT_ID=$2
COMMAND="$1"
TENANT_ID="${2:-}"
case "$COMMAND" in
-h|--help)
show_help
;;
add)
if [ -z "$TENANT_ID" ]; then
echo "Error: Please provide a tenant_id."
exit 1
fi
echo "Adding new tenant '$TENANT_ID'..."
# Add Nginx configuration
if [ -f "docker/nginx/multitenant.conf" ]; then
echo "Assuming dynamic routing based on subdomain ($TENANT_ID)..."
PORT_ARG="$3"
if [ -n "$PORT_ARG" ]; then
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
echo "Adding new tenant '$TENANT_ID'..."
# Initialize tenant database via Python inside container
echo "Initializing database for $TENANT_ID..."
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
@@ -79,9 +278,21 @@ client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
client.drop_database(f'{settings.MONGODB_DB}_{sys.argv[1]}')
print(f'Database for tenant {sys.argv[1]} dropped.')
" "$TENANT_ID"
echo "Tenant '$TENANT_ID' removed successfully."
echo "Tenant '$TENANT_ID' database removed."
else
echo "Error: Application container not running."
echo "Warning: Application container not running. Tenant database may still exist in MongoDB."
fi
port_to_remove=""
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
if [ -n "$port_to_remove" ]; then
remove_runtime_port "$port_to_remove"
echo "Removed tenant '$TENANT_ID' from config.json and cleaned runtime port $port_to_remove."
else
echo "Removed tenant '$TENANT_ID' from config.json. No port mapping was present."
fi
else
echo "Warning: tenant '$TENANT_ID' was not configured in config.json or could not be removed."
fi
else
echo "Removal canceled."
@@ -118,17 +329,46 @@ print(f'Tenant {sys.argv[1]} session cache cleared. Tenant restarted.')
;;
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)
if [ -n "$APP_CONTAINER" ]; then
docker exec $APP_CONTAINER python3 -c "
import sys; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient
docker exec -i "$APP_CONTAINER" python3 - <<'PY'
import sys
sys.path.insert(0, '/app/Web')
import settings
from pymongo import MongoClient
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
prefix = f'{settings.MONGODB_DB}_'
dbs = [d for d in client.list_database_names() if d.startswith(prefix)]
for db in dbs:
print(f'- {db.replace(prefix, \"\")}')
"
if dbs:
for db in dbs:
print(f'- {db.replace(prefix, "")}')
else:
print(' (no tenant databases found)')
PY
else
echo "Error: Application container not running."
fi
+51 -11
View File
@@ -1,14 +1,54 @@
#!/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
#!/usr/bin/env bash
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" "$@"
"$SCRIPT_DIR/start.sh" "$@"
COMPOSE_FILE="docker-compose-multitenant.yml"
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."
+115 -186
View File
@@ -14,11 +14,15 @@ if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
IS_ROOT="false"
if [ "$(id -u)" -eq 0 ]; then
IS_ROOT="true"
fi
NUITKA_BUILD_VALUE="0"
HTTP_PORT_VALUE="10000"
HTTPS_PORT_VALUE="10001"
HTTP_PORTS_VALUE=""
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"
@@ -116,7 +120,11 @@ ensure_runtime_dependencies() {
local missing=()
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
if ! docker compose version >/dev/null 2>&1; then
@@ -140,14 +148,20 @@ ensure_runtime_dependencies() {
fi
if [ "${#missing[@]}" -gt 0 ]; then
echo "Installing missing dependencies: ${missing[*]}"
apt_install "${missing[@]}"
if [ "$IS_ROOT" = "true" ]; then
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
if command -v systemctl >/dev/null 2>&1; then
$SUDO systemctl enable --now docker >/dev/null 2>&1 || true
if [ "$IS_ROOT" = "true" ] && command -v systemctl >/dev/null 2>&1; then
systemctl enable --now docker >/dev/null 2>&1 || true
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
}
@@ -160,6 +174,11 @@ setup_boot_autostart_service() {
return 0
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
return 0
fi
@@ -201,6 +220,11 @@ setup_scheduled_jobs() {
return 0
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
echo "Warning: crontab not available, skipping nightly update setup"
return 0
@@ -211,101 +235,17 @@ setup_scheduled_jobs() {
backup_line="30 2 * * * cd $SCRIPT_DIR && ./backup.sh --mode auto >> $SCRIPT_DIR/logs/backup.log 2>&1"
local existing_cron
if [ "$(id -u)" -eq 0 ]; then
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
echo "$backup_line"
echo "$update_line"
} | 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
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
echo "$backup_line"
echo "$update_line"
} | crontab -
echo "Nightly backup scheduled at 02:30"
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() {
local config_path backup_path
config_path="$SCRIPT_DIR/config.json"
@@ -322,7 +262,7 @@ ensure_runtime_config_json() {
"ver": "2.6.5",
"dbg": false,
"host": "0.0.0.0",
"port": 443,
"port": 8000,
"mongodb": {
"host": "mongodb",
"port": 27017,
@@ -443,16 +383,6 @@ resolve_app_image() {
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() {
local port="$1"
@@ -471,89 +401,76 @@ find_free_port() {
echo "$port"
}
stack_owns_host_port() {
local requested_port="$1"
local container_port="$2"
local mapped_port
mapped_port="$(docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" port nginx "$container_port" 2>/dev/null | tail -n1 || true)"
if [ -z "$mapped_port" ]; then
return 1
fi
mapped_port="${mapped_port##*:}"
[ "$mapped_port" = "$requested_port" ]
}
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
}
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))
parse_port_list() {
local raw="$1"
local port
local ports=()
raw="${raw//,/ }"
for port in $raw; do
port="${port//[[:space:]]/}"
if [ -n "$port" ] && printf '%s\n' "$port" | grep -qE '^[0-9]+$'; then
ports+=("$port")
fi
done
echo "$port"
printf '%s\n' "${ports[@]}"
}
configure_host_ports() {
local requested_http requested_https base_port
local requested_http
local requested_ports
local ports=()
local occupied_ports=()
requested_http=""
requested_https=""
requested_ports=""
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)"
requested_ports="$(awk -F= '/^INVENTAR_HTTP_PORTS=/{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="$((requested_http + DEFAULT_TENANT_TLS_OFFSET))"
if [ -n "${INVENTAR_HTTP_PORTS:-}" ]; then
requested_ports="$INVENTAR_HTTP_PORTS"
fi
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"
return
if [ -n "${INVENTAR_HTTP_PORT:-}" ] && [ -z "$requested_ports" ]; then
requested_ports="$INVENTAR_HTTP_PORT"
fi
if ! port_in_use "$requested_http" && ! port_in_use "$requested_https"; then
HTTP_PORT_VALUE="$requested_http"
HTTPS_PORT_VALUE="$requested_https"
return
if [ -n "$requested_ports" ]; then
mapfile -t ports < <(parse_port_list "$requested_ports")
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"
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
else
HTTP_PORT_VALUE="$DEFAULT_TENANT_PORT_START"
HTTP_PORTS_VALUE="$HTTP_PORT_VALUE"
if port_in_use "$HTTP_PORT_VALUE"; then
HTTP_PORT_VALUE="$(find_free_port "$DEFAULT_TENANT_PORT_START")"
echo "Host port $DEFAULT_TENANT_PORT_START is already occupied. Assigned new tenant port: $HTTP_PORT_VALUE"
HTTP_PORTS_VALUE="$HTTP_PORT_VALUE"
fi
fi
}
ensure_min_docker_disk_space() {
@@ -591,7 +508,7 @@ write_env_file() {
cat > "$ENV_FILE" <<EOF
NUITKA_BUILD=$NUITKA_BUILD_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
EOF
}
@@ -605,6 +522,21 @@ services:
image: ${APP_IMAGE_VALUE}
build: null
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() {
@@ -621,10 +553,10 @@ verify_stack_health() {
for _ in $(seq 1 60); do
running_services="$(docker compose "${compose_args[@]}" ps --status running --services 2>/dev/null || true)"
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
if docker compose "${compose_args[@]}" exec -T app python3 -c "import flask, pymongo" >/dev/null 2>&1; then
if curl -kfsSL "https://127.0.0.1:$HTTPS_PORT_VALUE/health" >/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."
return 0
fi
@@ -637,8 +569,8 @@ verify_stack_health() {
if [[ $retry_count -eq 0 ]]; then
echo "Health check failed. Attempting to restart containers..."
docker compose "${compose_args[@]}" ps || true
docker compose "${compose_args[@]}" logs --tail=120 app nginx mongodb || true
docker compose "${compose_args[@]}" restart app nginx mongodb
docker compose "${compose_args[@]}" logs --tail=120 app redis mongodb || true
docker compose "${compose_args[@]}" restart app redis mongodb
sleep 3
((retry_count++))
else
@@ -649,7 +581,7 @@ verify_stack_health() {
# Final failure
echo "Error: stack health check failed after restart attempt."
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
}
@@ -657,13 +589,10 @@ parse_args "$@"
ensure_runtime_dependencies
setup_boot_autostart_service
ensure_tls_certificates
ensure_nginx_config_mount_source
ensure_runtime_config_json
setup_scheduled_jobs
configure_nuitka_mode
resolve_app_image
configure_cloudflared_profile
configure_host_ports
ensure_min_docker_disk_space
ensure_app_image_loaded
@@ -688,4 +617,4 @@ fi
verify_stack_health
echo "Stack started."
echo "Open: https://<server-ip>:$HTTPS_PORT_VALUE"
echo "Open: http://<server-ip>:$HTTP_PORT_VALUE"
+24 -96
View File
@@ -38,6 +38,11 @@ if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
IS_ROOT="false"
if [ "$(id -u)" -eq 0 ]; then
IS_ROOT="true"
fi
apt_install() {
$SUDO apt-get update -y
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"
@@ -47,7 +52,7 @@ ensure_runtime_dependencies() {
local missing=()
if ! command -v docker >/dev/null 2>&1; then
missing+=(docker.io)
missing+=(docker)
fi
if ! docker compose version >/dev/null 2>&1; then
@@ -67,87 +72,18 @@ ensure_runtime_dependencies() {
fi
if [ "${#missing[@]}" -gt 0 ]; then
log_message "Installing missing dependencies: ${missing[*]}"
apt_install "${missing[@]}"
if [ "$IS_ROOT" = "true" ]; then
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
if command -v systemctl >/dev/null 2>&1; then
$SUDO systemctl enable --now docker >/dev/null 2>&1 || true
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'
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
log_message "Recreated missing nginx config at $config_path"
if [ "$IS_ROOT" = "true" ] && command -v systemctl >/dev/null 2>&1; then
systemctl enable --now docker >/dev/null 2>&1 || true
fi
}
@@ -408,9 +344,7 @@ download_and_extract_bundle() {
tar -xzf "$archive" -C "$tmp_dir"
# 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/nginx/default.conf" "$PROJECT_DIR/docker/nginx/default.conf"
cp -f "$tmp_dir/start.sh" "$PROJECT_DIR/start.sh"
cp -f "$tmp_dir/stop.sh" "$PROJECT_DIR/stop.sh"
@@ -426,9 +360,6 @@ download_and_extract_bundle() {
if [ -f "$tmp_dir/docker-compose-multitenant.yml" ]; then
cp -f "$tmp_dir/docker-compose-multitenant.yml" "$PROJECT_DIR/docker-compose-multitenant.yml"
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
cp -f "$tmp_dir/manage-tenant.sh" "$PROJECT_DIR/manage-tenant.sh"
fi
@@ -470,7 +401,6 @@ deploy() {
cat > "$ENV_FILE" <<EOF
NUITKA_BUILD=0
INVENTAR_HTTP_PORT=10000
INVENTAR_HTTPS_PORT=10001
INVENTAR_APP_IMAGE=$app_image
EOF
elif grep -q '^INVENTAR_APP_IMAGE=' "$ENV_FILE"; then
@@ -489,27 +419,27 @@ EOF
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 tag "$app_image" "$APP_IMAGE_REPO:latest" >> "$LOG_FILE" 2>&1 || true
}
verify_stack_health() {
local compose_args running_services
local https_port
local http_port
compose_args=(-f "$PROJECT_DIR/$COMPOSE_FILE" --env-file "$ENV_FILE")
https_port="$(awk -F= '/^INVENTAR_HTTPS_PORT=/{print $2}' "$ENV_FILE" | tr -d ' ')"
if [ -z "$https_port" ]; then
https_port="443"
http_port="$(awk -F= '/^INVENTAR_HTTP_PORT=/{print $2}' "$ENV_FILE" | tr -d ' ')"
if [ -z "$http_port" ]; then
http_port="10000"
fi
for _ in $(seq 1 60); do
running_services="$(docker compose "${compose_args[@]}" ps --status running --services 2>/dev/null || true)"
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
# Primary check: health endpoint responds (most reliable)
if curl -kfsSL "https://127.0.0.1:$https_port/health" >/dev/null 2>&1; then
if curl -fsS "http://127.0.0.1:$http_port/health" >/dev/null 2>&1; then
return 0
fi
fi
@@ -517,7 +447,7 @@ verify_stack_health() {
done
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
}
@@ -545,8 +475,6 @@ main() {
cleanup_server_space
ensure_runtime_dependencies
ensure_tls_certificates
ensure_nginx_config_mount_source
require_cmd curl
require_cmd tar