-# ---------- STAGE 1: BUILD ----------
-FROM python:3.12-slim AS builder
+# ---------- STAGE 1: Alpine Build ----------
+FROM python:3.12-alpine AS builder
# Install build dependencies
-RUN apt-get update && \
- apt-get install -y --no-install-recommends sqlite3 && \
- apt-get clean && \
- rm -rf /var/lib/apt/lists/*
+RUN apk add --no-cache build-base libffi-dev openssl-dev
+WORKDIR /app
+
+# Copy dependency list
COPY requirements.txt .
-RUN pip install --prefix=/install -r requirements.txt
-WORKDIR /app
+# Build wheels to avoid building in final image
+RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
-# Copy backend, frontend, entrypoint
+# Copy full application
COPY backend backend
COPY frontend frontend
-COPY entrypoint.py entrypoint.py
COPY log log
COPY settings settings
-RUN chmod 755 entrypoint.py
-# ---------- STAGE 2: DISTROLESS ----------
-FROM gcr.io/distroless/base-debian13
+# ---------- STAGE 2: Alpine Runtime ----------
+FROM python:3.12-alpine
-# Copy Python runtime from builder
-COPY --from=builder /usr/local /usr/local
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ LANG=C.UTF-8
-# Copy libs
-COPY --from=builder /lib/x86_64-linux-gnu/libsqlite3.so.0 /lib/x86_64-linux-gnu/
-COPY --from=builder /lib/x86_64-linux-gnu/libz.so.1 /lib/x86_64-linux-gnu/
-COPY --from=builder /lib/x86_64-linux-gnu/libbz2.so.1.0 /lib/x86_64-linux-gnu/
-COPY --from=builder /lib/x86_64-linux-gnu/liblzma.so.5 /lib/x86_64-linux-gnu/
-COPY --from=builder /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/
-
-# Copy installed Python packages
-COPY --from=builder /install /usr/local
+# librerie runtime
+RUN apk add --no-cache libffi openssl sqlite-libs
WORKDIR /app
-# Copy application
-COPY --from=builder /app/backend backend
-COPY --from=builder /app/frontend frontend
-COPY --from=builder /app/entrypoint.py entrypoint.py
-COPY --from=builder /app/log log
-COPY --from=builder /app/settings settings
+# Copy application and deps
+COPY --from=builder /app /app
+COPY --from=builder /wheels /wheels
+
+# Install dependencies inside distroless environment
+RUN pip install --no-cache-dir --no-compile /wheels/* && \
+ rm -rf /wheels && \
+ find /usr/local/lib/python3.12/site-packages -regex '.*\(tests\|test\|docs\|examples\).*' -type d -prune -exec rm -rf {} + && \
+ rm -rf /root/.cache
-# Ensure Python sees the installed packages
-ENV PYTHONPATH="/usr/local/lib/python3.12/site-packages"
+RUN find /usr/local/lib/python3.12 -name '__pycache__' -type d -exec rm -rf {} +
-ENTRYPOINT ["/app/entrypoint.py"]
-CMD ["python3", "-u", "-m", "backend.main"]
+ENTRYPOINT ["python", "-u", "-m", "backend.main"]
--- /dev/null
+# backend/bootstrap.py
+
+# Import standard modules
+import logging
+import os
+# Import backend modules
+from backend.db.db import init_db
+import backend.db.users
+import backend.db.hosts
+import backend.db.aliases
+# Import Settings & Logging
+from settings.settings import settings
+from log.log import setup_logging, get_logger
+
+# ------------------------------------------------------------------------------
+# Welcome log
+# ------------------------------------------------------------------------------
+def print_welcome(logger):
+ masked_secret = "****" if settings.SECRET_KEY else "undefined"
+ masked_admin_pwd = "****" if settings.ADMIN_PASSWORD else "undefined"
+ masked_admin_hash = "****" if settings.ADMIN_PASSWORD_HASH else "undefined"
+
+ logger.info(
+ "%s starting | app_version=%s",
+ settings.APP_NAME, settings.APP_VERSION
+ )
+ logger.info(
+ "App settings: frontend=%s | host=%s | port=%d | secret=%s",
+ settings.FRONTEND_DIR, settings.HTTP_HOST, settings.HTTP_PORT, masked_secret
+ )
+ logger.info(
+ "Database: file=%s | reset=%s",
+ settings.DB_FILE, settings.DB_RESET
+ )
+ logger.info(
+ "Log: level=%s, to_file=%s, file=%s",
+ settings.LOG_LEVEL, settings.LOG_TO_FILE, settings.LOG_FILE
+ )
+ logger.info(
+ "Users: admin=%s | password=%s | hash=%s | hash_file=%s",
+ settings.ADMIN_USER, masked_admin_pwd, masked_admin_hash, settings.ADMIN_PASSWORD_HASH_FILE
+ )
+ logger.info(
+ "DNS: host file=%s | alias file=%s | reverse file=%s",
+ settings.DNS_HOST_FILE, settings.DNS_ALIAS_FILE, settings.DNS_REVERSE_FILE
+ )
+ logger.info(
+ "DHCP: ipv4 host file=%s | ipv4 leases file=%s | ipv6 host file=%s | ipv6 leases file=%s",
+ settings.DHCP4_HOST_FILE, settings.DHCP4_LEASES_FILE, settings.DHCP6_HOST_FILE, settings.DHCP6_LEASES_FILE
+ )
+
+# ------------------------------------------------------------------------------
+# Shutdown log
+# ------------------------------------------------------------------------------
+def print_goodbye(logger):
+ logger.info(
+ "Application %s shutting down | app_version=%s",
+ settings.APP_NAME, settings.APP_VERSION
+ )
+
+# ================================
+# Create DB if needed
+# ================================
+def docker_create_db(logger):
+ # Reset database if requested
+ if settings.DB_RESET and os.path.exists(settings.DB_FILE):
+ logger.info("Removing existing database: %s", settings.DB_FILE)
+ os.remove(settings.DB_FILE)
+
+ # Skip creation if DB already exists
+ if os.path.exists(settings.DB_FILE):
+ logger.info("Database already exists. Nothing to do.")
+ return
+
+ logger.info("Creating database: %s", settings.DB_FILE)
+
+ # Ensure directory exists
+ os.makedirs(os.path.dirname(settings.DB_FILE) or ".", exist_ok=True)
+
+ # Initialize DB tables
+ init_db()
+
+# ------------------------------------------------------------------------------
+# Bootstrap: setup logging, print welcome, create DB, etc.
+# ------------------------------------------------------------------------------
+def bootstrap():
+
+ # Log Setup
+ setup_logging(level=settings.LOG_LEVEL, to_file=settings.LOG_TO_FILE, log_file=settings.LOG_FILE, log_access_file=settings.LOG_ACCESS_FILE)
+ logger = get_logger(__name__)
+
+ print_welcome(logger)
+
+ # Create or update database
+ docker_create_db(logger)
+
+ #os.makedirs(DATA_DIR, exist_ok=True)
+ #os.makedirs(BIND_DIR, exist_ok=True)
+ #os.makedirs(KEA_DIR, exist_ok=True)
+
+ #if not os.path.exists(DB_PATH):
+ # conn = sqlite3.connect(DB_PATH)
+ # # eventuale executescript(schema) qui
+ # conn.close()
+
+ #named_conf = os.path.join(BIND_DIR, "named.conf")
+ #if not os.path.exists(named_conf):
+ # with open(named_conf, "w") as f:
+ # f.write("// generated by network-manager\n")
+
+ #kea_conf = os.path.join(KEA_DIR, "kea-dhcp4.conf")
+ #if not os.path.exists(kea_conf):
+ # with open(kea_conf, "w") as f:
+ # f.write("{\n // generated by network-manager\n}\n")
--- /dev/null
+# backend/main.py
+
+# Import standard modules
+import os
+# Import backend modules
+from backend.bootstrap import bootstrap
+from backend.app import create_app
+from backend.server import run_server
+
+# ------------------------------------------------------------------------------
+# Main: entry point of the application
+# ------------------------------------------------------------------------------
+def main():
+
+ # 1) System Initialization (Settings, Logging, DB, etc.)
+ bootstrap()
+
+ # 2) Costruzione app FastAPI
+ app = create_app()
+
+ # 4) Uvicorn Start
+ run_server(app)
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+# backend/server.py
+
+# import standard modules
+import uvicorn
+
+# Import Settings & Logging
+from settings.settings import settings
+from log.log import get_logger
+
+# Logger initialization
+logger = get_logger(__name__)
+
+# ------------------------------------------------------------------------------
+# Starting the server with Uvicorn
+# ------------------------------------------------------------------------------
+def run_server(app):
+
+ # Uvicorn config da settings with fallback
+ host=(settings.HTTP_HOST or "0.0.0.0")
+ port=int(settings.HTTP_PORT or 8000)
+ log_level=(settings.LOG_LEVEL or "info").lower()
+ workers = 1 # GRGR in prod valuta gunicorn+uvicorn workers
+ #reload = os.getenv("UVICORN_RELOAD", "false").lower() == "true"
+ reload = bool(getattr(settings, "DEV_RELOAD", False))
+
+ logger.info(f"Server running on http://{host}:{port} (reload={reload}, log_level={log_level})")
+
+ uvicorn.run(
+ app,
+ host=host,
+ port=port,
+ proxy_headers=True,
+ forwarded_allow_ips="*",
+ log_level=log_level,
+ #access_log=True,
+ log_config=None,
+ workers=(1 if reload else workers),
+ reload=reload,
+ )
+++ /dev/null
-#!/usr/local/bin/python3
-
-# Import standard modules
-import logging
-import os
-import sys
-import argparse
-# Import local modules
-from backend.db.db import init_db
-import backend.db.users
-import backend.db.hosts
-import backend.db.aliases
-# Import Settings
-from settings.settings import settings
-# Import Log
-from log.log import setup_logging, get_logger
-
-# ================================
-# Parse CLI arguments
-# ================================
-def parse_args():
- parser = argparse.ArgumentParser(add_help=False)
- parser.add_argument("--reset", action="store_true")
- parser.add_argument("--domain")
- parser.add_argument("--external-name")
- parser.add_argument("cmd", nargs=argparse.REMAINDER)
- return parser.parse_args()
-
-# ================================
-# Create DB if needed
-# ================================
-def docker_create_db(logger):
- # Reset database if requested
- if settings.DB_RESET and os.path.exists(settings.DB_FILE):
- logger.info("Removing existing database: %s", settings.DB_FILE)
- os.remove(settings.DB_FILE)
-
- # Skip creation if DB already exists
- if os.path.exists(settings.DB_FILE):
- logger.info("Database already exists. Nothing to do.")
- return
-
- logger.info("Creating database: %s", settings.DB_FILE)
-
- # Ensure directory exists
- os.makedirs(os.path.dirname(settings.DB_FILE) or ".", exist_ok=True)
-
- # Initialize DB tables
- init_db()
-
-# ================================
-# Entry Point
-# ================================
-def main():
- # Enable logging
- setup_logging()
- logger = get_logger("baseimg")
-
- # Log startup docker image
- logger.info("Starting docker image %s version %s", settings.BASEIMG_NAME, settings.BASEIMG_VERSION)
-
- # Parse arguments
- args = parse_args()
-
- # Apply arguments into settings
- if args.reset:
- settings.DB_RESET = True
- if args.domain:
- settings.DOMAIN = args.domain
- if args.external_name:
- settings.EXTERNAL_NAME = args.EXTERNAL_NAME
-
- # Create or update database
- docker_create_db(logger)
-
- # If no command provided -> error
- if not args.cmd:
- logger.error("No command provided. Exiting.")
- sys.exit(1)
-
- cmd = args.cmd[0]
- rest = args.cmd[1:]
-
- logger.info("Docker image initialization completed — executing: %s %s", cmd, " ".join(rest))
-
- try:
- os.execvp(cmd, [cmd, *rest])
- except FileNotFoundError:
- logger.critical("Command not found: %s", cmd)
- sys.exit(1)
-
-if __name__ == "__main__":
- main()