diff --git a/deployment/docker/backend/.env.example b/deployment/docker/backend/.env.example new file mode 100644 index 0000000..467d2c9 --- /dev/null +++ b/deployment/docker/backend/.env.example @@ -0,0 +1,34 @@ +###BEGIN GENERAL### +STACK_NAME=stk-depl0y-backend-001 +STACK_BINDMOUNTROOT=custom/docker/stacks +TZ=America/New_York +UID=0 +GID=0 +SERVICE_BIND_ADDRESS_EXTERNAL=0.0.0.0 +SERVICE_BIND_ADDRESS_INTERNAL=127.0.0.1 +DNSSERVER=1.1.1.1 +###END GENERAL### + +###BEGIN BACKEND### +BACKEND_IMAGENAME=Agit8or/Depl0y-backend +BACKEND_IMAGEVERSION=latest +BACKEND_ENABLEAUTOMATICUPDATES=true +BACKEND_PORT_EXTERNAL=8081 +###END BACKEND### + +###BEGIN APPLICATION### +SECRET_KEY=changeme +ENCRYPTION_KEY=changeme +LOG_LEVEL=INFO +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin + +# Optional path overrides inside the container +DATABASE_URL=sqlite:////var/lib/depl0y/db/depl0y.db +ISO_STORAGE_PATH=/var/lib/depl0y/isos +UPLOAD_DIR=/var/lib/depl0y +CLOUDINIT_TEMPLATE_PATH=/var/lib/depl0y/cloud-init +SSH_KEY_PATH=/var/lib/depl0y/ssh_keys +LOG_FILE=/var/log/depl0y/app.log +###END APPLICATION### + diff --git a/deployment/docker/backend/Dockerfile b/deployment/docker/backend/Dockerfile new file mode 100644 index 0000000..b033008 --- /dev/null +++ b/deployment/docker/backend/Dockerfile @@ -0,0 +1,61 @@ +FROM python:3.11-slim + +ARG APP_VERSION=1.1.9 + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + APP_VERSION=${APP_VERSION} + +# System dependencies required by the backend (SSH, PDF, etc.) +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + wget \ + sshpass \ + openssh-client \ + ca-certificates \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libgdk-pixbuf2.0-0 \ + libffi-dev \ + shared-mime-info \ + && rm -rf /var/lib/apt/lists/* + +# Create application directories +RUN mkdir -p /opt/depl0y/backend \ + /var/lib/depl0y/db \ + /var/lib/depl0y/cloud-images \ + /var/lib/depl0y/isos \ + /var/lib/depl0y/ssh_keys \ + /var/lib/depl0y/cloud-init \ + /var/log/depl0y + +WORKDIR /opt/depl0y/backend + +# Install Python dependencies +COPY src/backend/requirements.txt /opt/depl0y/backend/requirements.txt +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt \ + && pip install --no-cache-dir weasyprint markdown requests + +# Copy backend code +COPY src/backend /opt/depl0y/backend + +# Default environment values (can be overridden at runtime) +ENV DATABASE_URL=sqlite:////var/lib/depl0y/db/depl0y.db \ + ISO_STORAGE_PATH=/var/lib/depl0y/isos \ + UPLOAD_DIR=/var/lib/depl0y \ + CLOUDINIT_TEMPLATE_PATH=/var/lib/depl0y/cloud-init \ + SSH_KEY_PATH=/var/lib/depl0y/ssh_keys \ + LOG_FILE=/var/log/depl0y/app.log \ + APPLICATION_PORT_INTERNAL=8080 + +# Entrypoint script +COPY deployment/docker/backend/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +EXPOSE 8080 + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] + diff --git a/deployment/docker/backend/build-and-push.sh b/deployment/docker/backend/build-and-push.sh new file mode 100644 index 0000000..034d331 --- /dev/null +++ b/deployment/docker/backend/build-and-push.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" + +IMAGE_NAME="${IMAGE_NAME:-Agit8or/Depl0y-backend}" +APP_VERSION="${APP_VERSION:-$(date +%Y.%m.%d.%H%M)}" +VERSION_TAG="${IMAGE_TAG:-${APP_VERSION}}" + +echo "Building ${IMAGE_NAME}:${VERSION_TAG} and ${IMAGE_NAME}:latest from ${ROOT_DIR}..." +docker build \ + -f "${ROOT_DIR}/deployment/docker/backend/Dockerfile" \ + --build-arg APP_VERSION="${VERSION_TAG}" \ + -t "${IMAGE_NAME}:${VERSION_TAG}" \ + -t "${IMAGE_NAME}:latest" \ + "${ROOT_DIR}" + +if [ "${PUSH:-false}" = "true" ]; then + echo "Pushing ${IMAGE_NAME}:${VERSION_TAG} and ${IMAGE_NAME}:latest..." + docker push "${IMAGE_NAME}:${VERSION_TAG}" + docker push "${IMAGE_NAME}:latest" +fi + +echo "Done." + diff --git a/deployment/docker/backend/docker-compose.yml b/deployment/docker/backend/docker-compose.yml new file mode 100644 index 0000000..fff235a --- /dev/null +++ b/deployment/docker/backend/docker-compose.yml @@ -0,0 +1,59 @@ +name: '${STACK_NAME:-stk-depl0y-backend-001}' + +networks: + EXTERNAL: + name: DEPL0Y-BACKEND-EXTERNAL + driver: bridge + internal: false + attachable: true + + INTERNAL: + name: DEPL0Y-BACKEND-INTERNAL + driver: bridge + internal: true + attachable: true + +services: + Backend: + image: '${BACKEND_IMAGENAME:-Agit8or/Depl0y-backend}:${BACKEND_IMAGEVERSION:-latest}' + container_name: DEPL0Y-BACKEND-001 + hostname: DEPL0Y-BACKEND-001 + restart: unless-stopped + stop_signal: SIGTERM + stop_grace_period: 90s + user: '${UID:-0}:${GID:-0}' + logging: + driver: 'local' + #env_file: '.env' + environment: + SECRET_KEY: '${SECRET_KEY:-changeme}' + ENCRYPTION_KEY: '${ENCRYPTION_KEY:-changeme}' + LOG_LEVEL: '${LOG_LEVEL:-INFO}' + DATABASE_URL: '${DATABASE_URL:-sqlite:////var/lib/depl0y/db/depl0y.db}' + ISO_STORAGE_PATH: '${ISO_STORAGE_PATH:-/var/lib/depl0y/isos}' + UPLOAD_DIR: '${UPLOAD_DIR:-/var/lib/depl0y}' + CLOUDINIT_TEMPLATE_PATH: '${CLOUDINIT_TEMPLATE_PATH:-/var/lib/depl0y/cloud-init}' + SSH_KEY_PATH: '${SSH_KEY_PATH:-/var/lib/depl0y/ssh_keys}' + LOG_FILE: '${LOG_FILE:-/var/log/depl0y/app.log}' + APPLICATION_PORT_INTERNAL: '${APPLICATION_PORT_INTERNAL:-8080}' + ADMIN_USERNAME: '${ADMIN_USERNAME:-admin}' + ADMIN_PASSWORD: '${ADMIN_PASSWORD:-admin}' + networks: + INTERNAL: + EXTERNAL: + ports: + - "${SERVICE_BIND_ADDRESS_EXTERNAL:-0.0.0.0}:${BACKEND_PORT_EXTERNAL:-8081}:${APPLICATION_PORT_INTERNAL:-8080}" + dns: + - '${DNSSERVER:-127.0.0.53}' + volumes: + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-backend-001}/Backend/Data:/var/lib/depl0y:rw" + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-backend-001}/Backend/Logs:/var/log/depl0y:rw" + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-backend-001}/Backend/CloudInit:/var/lib/depl0y/cloud-init:rw" + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-backend-001}/Backend/SSHKeys:/var/lib/depl0y/ssh_keys:rw" + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-backend-001}/Backend/ISOs:/var/lib/depl0y/isos:rw" + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-backend-001}/Backend/CloudImages:/var/lib/depl0y/cloud-images:rw" + labels: + com.centurylinklabs.watchtower.enable: ${BACKEND_ENABLEAUTOMATICUPDATES:-true} + diff --git a/deployment/docker/backend/docker-entrypoint.sh b/deployment/docker/backend/docker-entrypoint.sh new file mode 100644 index 0000000..5564125 --- /dev/null +++ b/deployment/docker/backend/docker-entrypoint.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env sh +set -e + +APP_PORT="${APPLICATION_PORT_INTERNAL:-8080}" + +# Ensure core data directories exist (may be bind-mounted) +mkdir -p /var/lib/depl0y/db \ + /var/lib/depl0y/cloud-images \ + /var/lib/depl0y/isos \ + /var/lib/depl0y/ssh_keys \ + /var/lib/depl0y/cloud-init \ + /var/log/depl0y + +echo "Initializing database schema..." +python - <<'PY' +from app.core.database import init_db + +init_db() +PY +echo "Database schema initialized." + +echo "Initializing system settings..." +python /opt/depl0y/backend/init_system_settings.py || echo "Warning: system settings initialization failed" +echo "System settings initialization step completed." + +echo "Ensuring default admin user exists..." +python /opt/depl0y/backend/init_admin_user.py || echo "Warning: admin user initialization failed" +echo "Admin user initialization step completed." + +echo "Starting Depl0y backend with uvicorn on port ${APP_PORT}..." +exec uvicorn app.main:app --host 0.0.0.0 --port "${APP_PORT}" + diff --git a/deployment/docker/combined/.env.example b/deployment/docker/combined/.env.example new file mode 100644 index 0000000..0d2d7c4 --- /dev/null +++ b/deployment/docker/combined/.env.example @@ -0,0 +1,37 @@ +# Stack identification +STACK_NAME=stk-depl0y-001 +STACK_BINDMOUNTROOT=custom/docker/stacks + +# Image configuration +APPLICATION_IMAGENAME=Agit8or/Depl0y +APPLICATION_IMAGEVERSION=latest + +# Networking +SERVICE_BIND_ADDRESS_EXTERNAL=0.0.0.0 +APPLICATION_PORT_EXTERNAL=8080 +APPLICATION_PORT_INTERNAL=8080 +DNSSERVER=127.0.0.53 + +# Runtime user (mapped to host UID/GID) +UID=1000 +GID=1000 + +# Application secrets (change these!) +SECRET_KEY=changeme +ENCRYPTION_KEY=changeme +LOG_LEVEL=INFO +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin + +# Paths inside the container (normally leave at defaults) +DATABASE_URL=sqlite:////var/lib/depl0y/db/depl0y.db +ISO_STORAGE_PATH=/var/lib/depl0y/isos +UPLOAD_DIR=/var/lib/depl0y +CLOUDINIT_TEMPLATE_PATH=/var/lib/depl0y/cloud-init +SSH_KEY_PATH=/var/lib/depl0y/ssh_keys +LOG_FILE=/var/log/depl0y/app.log +FRONTEND_DIST_PATH=/opt/depl0y/frontend/dist + +# Watchtower / auto-update +APPLICATION_ENABLEAUTOMATICUPDATES=true + diff --git a/deployment/docker/combined/Dockerfile b/deployment/docker/combined/Dockerfile new file mode 100644 index 0000000..4cbd215 --- /dev/null +++ b/deployment/docker/combined/Dockerfile @@ -0,0 +1,64 @@ +FROM python:3.11-slim + +ARG APP_VERSION=1.1.9 + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + APP_VERSION=${APP_VERSION} + +# System dependencies required by the backend (SSH, PDF, etc.) +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + wget \ + sshpass \ + openssh-client \ + ca-certificates \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libgdk-pixbuf2.0-0 \ + libffi-dev \ + shared-mime-info \ + && rm -rf /var/lib/apt/lists/* + +# Create application directories +RUN mkdir -p /opt/depl0y/backend \ + /opt/depl0y/frontend/dist \ + /var/lib/depl0y/db \ + /var/lib/depl0y/cloud-images \ + /var/lib/depl0y/isos \ + /var/lib/depl0y/ssh_keys \ + /var/lib/depl0y/cloud-init \ + /var/log/depl0y + +WORKDIR /opt/depl0y/backend + +# Install Python dependencies +COPY src/backend/requirements.txt /opt/depl0y/backend/requirements.txt +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt \ + && pip install --no-cache-dir weasyprint markdown requests + +# Copy backend code and built frontend assets +COPY src/backend /opt/depl0y/backend +COPY src/frontend/dist /opt/depl0y/frontend/dist + +# Default environment values (can be overridden at runtime) +ENV DATABASE_URL=sqlite:////var/lib/depl0y/db/depl0y.db \ + ISO_STORAGE_PATH=/var/lib/depl0y/isos \ + UPLOAD_DIR=/var/lib/depl0y \ + CLOUDINIT_TEMPLATE_PATH=/var/lib/depl0y/cloud-init \ + SSH_KEY_PATH=/var/lib/depl0y/ssh_keys \ + LOG_FILE=/var/log/depl0y/app.log \ + FRONTEND_DIST_PATH=/opt/depl0y/frontend/dist \ + APPLICATION_PORT_INTERNAL=8080 + +# Entrypoint script +COPY deployment/docker/combined/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +EXPOSE 8080 + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] + diff --git a/deployment/docker/combined/build-and-push.sh b/deployment/docker/combined/build-and-push.sh new file mode 100644 index 0000000..7968dc4 --- /dev/null +++ b/deployment/docker/combined/build-and-push.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" + +IMAGE_NAME="${IMAGE_NAME:-Agit8or/Depl0y}" +APP_VERSION="${APP_VERSION:-$(date +%Y.%m.%d.%H%M)}" +VERSION_TAG="${IMAGE_TAG:-${APP_VERSION}}" + +echo "Building ${IMAGE_NAME}:${VERSION_TAG} and ${IMAGE_NAME}:latest from ${ROOT_DIR}..." +docker build \ + -f "${ROOT_DIR}/deployment/docker/combined/Dockerfile" \ + --build-arg APP_VERSION="${VERSION_TAG}" \ + -t "${IMAGE_NAME}:${VERSION_TAG}" \ + -t "${IMAGE_NAME}:latest" \ + "${ROOT_DIR}" + +if [ "${PUSH:-false}" = "true" ]; then + echo "Pushing ${IMAGE_NAME}:${VERSION_TAG} and ${IMAGE_NAME}:latest..." + docker push "${IMAGE_NAME}:${VERSION_TAG}" + docker push "${IMAGE_NAME}:latest" +fi + +echo "Done." + diff --git a/deployment/docker/combined/docker-compose.yml b/deployment/docker/combined/docker-compose.yml new file mode 100644 index 0000000..56d4aa3 --- /dev/null +++ b/deployment/docker/combined/docker-compose.yml @@ -0,0 +1,60 @@ +name: '${STACK_NAME:-stk-depl0y-001}' + +networks: + EXTERNAL: + name: DEPL0Y-EXTERNAL + driver: bridge + internal: false + attachable: true + + INTERNAL: + name: DEPL0Y-INTERNAL + driver: bridge + internal: true + attachable: true + +services: + Application: + image: '${APPLICATION_IMAGENAME:-Agit8or/Depl0y}:${APPLICATION_IMAGEVERSION:-latest}' + container_name: DEPL0Y-APP-001 + hostname: DEPL0Y-APP-001 + restart: unless-stopped + stop_signal: SIGTERM + stop_grace_period: 90s + user: '${UID:-0}:${GID:-0}' + logging: + driver: 'local' + #env_file: '.env' + environment: + SECRET_KEY: '${SECRET_KEY:-changeme}' + ENCRYPTION_KEY: '${ENCRYPTION_KEY:-changeme}' + LOG_LEVEL: '${LOG_LEVEL:-INFO}' + DATABASE_URL: '${DATABASE_URL:-sqlite:////var/lib/depl0y/db/depl0y.db}' + ISO_STORAGE_PATH: '${ISO_STORAGE_PATH:-/var/lib/depl0y/isos}' + UPLOAD_DIR: '${UPLOAD_DIR:-/var/lib/depl0y}' + CLOUDINIT_TEMPLATE_PATH: '${CLOUDINIT_TEMPLATE_PATH:-/var/lib/depl0y/cloud-init}' + SSH_KEY_PATH: '${SSH_KEY_PATH:-/var/lib/depl0y/ssh_keys}' + LOG_FILE: '${LOG_FILE:-/var/log/depl0y/app.log}' + FRONTEND_DIST_PATH: '${FRONTEND_DIST_PATH:-/opt/depl0y/frontend/dist}' + APPLICATION_PORT_INTERNAL: '${APPLICATION_PORT_INTERNAL:-8080}' + ADMIN_USERNAME: '${ADMIN_USERNAME:-admin}' + ADMIN_PASSWORD: '${ADMIN_PASSWORD:-admin}' + networks: + INTERNAL: + EXTERNAL: + ports: + - "${SERVICE_BIND_ADDRESS_EXTERNAL:-0.0.0.0}:${APPLICATION_PORT_EXTERNAL:-8080}:${APPLICATION_PORT_INTERNAL:-8080}" + dns: + - '${DNSSERVER:-127.0.0.53}' + volumes: + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-001}/Application/Data:/var/lib/depl0y:rw" + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-001}/Application/Logs:/var/log/depl0y:rw" + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-001}/Application/CloudInit:/var/lib/depl0y/cloud-init:rw" + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-001}/Application/SSHKeys:/var/lib/depl0y/ssh_keys:rw" + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-001}/Application/ISOs:/var/lib/depl0y/isos:rw" + - "/${STACK_BINDMOUNTROOT:-custom/docker/stacks}/${STACK_NAME:-stk-depl0y-001}/Application/CloudImages:/var/lib/depl0y/cloud-images:rw" + labels: + com.centurylinklabs.watchtower.enable: ${APPLICATION_ENABLEAUTOMATICUPDATES:-true} + diff --git a/deployment/docker/combined/docker-entrypoint.sh b/deployment/docker/combined/docker-entrypoint.sh new file mode 100644 index 0000000..adffc92 --- /dev/null +++ b/deployment/docker/combined/docker-entrypoint.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env sh +set -e + +APP_PORT="${APPLICATION_PORT_INTERNAL:-8080}" + +# Ensure core data directories exist (may be bind-mounted) +mkdir -p /var/lib/depl0y/db \ + /var/lib/depl0y/cloud-images \ + /var/lib/depl0y/isos \ + /var/lib/depl0y/ssh_keys \ + /var/lib/depl0y/cloud-init \ + /var/log/depl0y + +echo "Initializing database schema..." +python - <<'PY' +from app.core.database import init_db + +init_db() +PY +echo "Database schema initialized." + +echo "Initializing system settings..." +python /opt/depl0y/backend/init_system_settings.py || echo "Warning: system settings initialization failed" +echo "System settings initialization step completed." + +echo "Ensuring default admin user exists..." +python /opt/depl0y/backend/init_admin_user.py || echo "Warning: admin user initialization failed" +echo "Admin user initialization step completed." + +echo "Starting Depl0y with uvicorn on port ${APP_PORT}..." +exec uvicorn app.main:app --host 0.0.0.0 --port "${APP_PORT}" + diff --git a/deployment/docker/frontend/.env.example b/deployment/docker/frontend/.env.example new file mode 100644 index 0000000..44802cc --- /dev/null +++ b/deployment/docker/frontend/.env.example @@ -0,0 +1,18 @@ +###BEGIN GENERAL### +STACK_NAME=stk-depl0y-frontend-001 +STACK_BINDMOUNTROOT=custom/docker/stacks +TZ=America/New_York +UID=0 +GID=0 +SERVICE_BIND_ADDRESS_EXTERNAL=0.0.0.0 +SERVICE_BIND_ADDRESS_INTERNAL=127.0.0.1 +DNSSERVER=1.1.1.1 +###END GENERAL### + +###BEGIN FRONTEND### +FRONTEND_IMAGENAME=Agit8or/Depl0y-frontend +FRONTEND_IMAGEVERSION=latest +FRONTEND_ENABLEAUTOMATICUPDATES=true +FRONTEND_PORT_EXTERNAL=8080 +###END FRONTEND### + diff --git a/deployment/docker/frontend/Dockerfile b/deployment/docker/frontend/Dockerfile new file mode 100644 index 0000000..9e4b8aa --- /dev/null +++ b/deployment/docker/frontend/Dockerfile @@ -0,0 +1,16 @@ +FROM nginx:alpine + +ARG APP_VERSION=1.1.9 + +ENV APP_VERSION=${APP_VERSION} + +# Copy built frontend assets into nginx web root +COPY src/frontend/dist /usr/share/nginx/html + +# Custom nginx config for SPA routing (serve index.html for unknown routes) +COPY deployment/docker/frontend/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] + diff --git a/deployment/docker/frontend/build-and-push.sh b/deployment/docker/frontend/build-and-push.sh new file mode 100644 index 0000000..7fa640d --- /dev/null +++ b/deployment/docker/frontend/build-and-push.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" + +IMAGE_NAME="${IMAGE_NAME:-Agit8or/Depl0y-frontend}" +APP_VERSION="${APP_VERSION:-$(date +%Y.%m.%d.%H%M)}" +VERSION_TAG="${IMAGE_TAG:-${APP_VERSION}}" + +echo "Building ${IMAGE_NAME}:${VERSION_TAG} and ${IMAGE_NAME}:latest from ${ROOT_DIR}..." +docker build \ + -f "${ROOT_DIR}/deployment/docker/frontend/Dockerfile" \ + --build-arg APP_VERSION="${VERSION_TAG}" \ + -t "${IMAGE_NAME}:${VERSION_TAG}" \ + -t "${IMAGE_NAME}:latest" \ + "${ROOT_DIR}" + +if [ "${PUSH:-false}" = "true" ]; then + echo "Pushing ${IMAGE_NAME}:${VERSION_TAG} and ${IMAGE_NAME}:latest..." + docker push "${IMAGE_NAME}:${VERSION_TAG}" + docker.push "${IMAGE_NAME}:latest" +fi + +echo "Done." + diff --git a/deployment/docker/frontend/docker-compose.yml b/deployment/docker/frontend/docker-compose.yml new file mode 100644 index 0000000..3bd9254 --- /dev/null +++ b/deployment/docker/frontend/docker-compose.yml @@ -0,0 +1,33 @@ +name: '${STACK_NAME:-stk-depl0y-frontend-001}' + +networks: + EXTERNAL: + name: DEPL0Y-FRONTEND-EXTERNAL + driver: bridge + internal: false + attachable: true + +services: + Frontend: + image: '${FRONTEND_IMAGENAME:-Agit8or/Depl0y-frontend}:${FRONTEND_IMAGEVERSION:-latest}' + container_name: DEPL0Y-FRONTEND-001 + hostname: DEPL0Y-FRONTEND-001 + restart: unless-stopped + stop_signal: SIGTERM + stop_grace_period: 90s + user: '${UID:-0}:${GID:-0}' + logging: + driver: 'local' + #env_file: '.env' + networks: + EXTERNAL: + ports: + - "${SERVICE_BIND_ADDRESS_EXTERNAL:-0.0.0.0}:${FRONTEND_PORT_EXTERNAL:-8080}:80" + dns: + - '${DNSSERVER:-127.0.0.53}' + volumes: + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + labels: + com.centurylinklabs.watchtower.enable: ${FRONTEND_ENABLEAUTOMATICUPDATES:-true} + diff --git a/deployment/docker/frontend/nginx.conf b/deployment/docker/frontend/nginx.conf new file mode 100644 index 0000000..84bc207 --- /dev/null +++ b/deployment/docker/frontend/nginx.conf @@ -0,0 +1,12 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri /index.html; + } +} + diff --git a/src/backend/app/core/config.py b/src/backend/app/core/config.py index 6be3a23..43a7f5d 100644 --- a/src/backend/app/core/config.py +++ b/src/backend/app/core/config.py @@ -5,26 +5,33 @@ import os def get_app_version(): - """Get application version from database or fallback to default""" + """Get application version from env, database, or fallback to default.""" + # 1) Prefer explicit APP_VERSION env var (e.g. from Docker image build) + env_version = os.getenv("APP_VERSION") + if env_version: + return env_version + + # 2) Attempt to read from system_settings table in the database try: import sqlite3 db_path = os.getenv("DATABASE_URL", "sqlite:////var/lib/depl0y/db/depl0y.db") # Extract path from sqlite URL if db_path.startswith("sqlite:///"): db_path = db_path.replace("sqlite:///", "") - + conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute("SELECT value FROM system_settings WHERE key = 'app_version'") result = cursor.fetchone() conn.close() - + if result: return result[0] - except Exception as e: + except Exception: # Fallback to hardcoded version if database query fails pass - + + # 3) Final fallback to baked-in default return "1.1.9" @@ -93,6 +100,9 @@ class Settings(BaseSettings): LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO") LOG_FILE: str = os.getenv("LOG_FILE", "/var/log/depl0y/app.log") + # Frontend + FRONTEND_DIST_PATH: str = os.getenv("FRONTEND_DIST_PATH", "/opt/depl0y/frontend/dist") + class Config: case_sensitive = True env_file = ".env" diff --git a/src/backend/app/main.py b/src/backend/app/main.py index c635fc6..2256f74 100644 --- a/src/backend/app/main.py +++ b/src/backend/app/main.py @@ -2,7 +2,8 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.exceptions import RequestValidationError -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, FileResponse +from fastapi.staticfiles import StaticFiles from app.core.config import settings from app.core.database import init_db from app.api import auth, users, proxmox, vms, isos, cloud_images, updates, dashboard, bug_report, logs, docs, setup, system_updates, ha, system @@ -39,6 +40,27 @@ app.add_middleware( allow_headers=["*"], ) +FRONTEND_INDEX = os.path.join(settings.FRONTEND_DIST_PATH, "index.html") +FRONTEND_ASSETS_DIR = os.path.join(settings.FRONTEND_DIST_PATH, "assets") + +if os.path.isdir(settings.FRONTEND_DIST_PATH): + if os.path.isdir(FRONTEND_ASSETS_DIR): + app.mount( + "/assets", + StaticFiles(directory=FRONTEND_ASSETS_DIR), + name="assets", + ) + else: + logger.warning( + "Frontend assets directory '%s' not found; static assets will not be served.", + FRONTEND_ASSETS_DIR, + ) +else: + logger.warning( + "Frontend dist path '%s' does not exist; UI will not be served.", + settings.FRONTEND_DIST_PATH, + ) + # Add validation error handler for debugging @app.exception_handler(RequestValidationError) @@ -65,13 +87,20 @@ async def startup_event(): logger.info("Database initialized") -@app.get("/") +@app.get("/", include_in_schema=False, response_class=FileResponse) async def root(): - """Root endpoint""" + """Serve frontend SPA index.html or fallback to JSON status.""" + if os.path.exists(FRONTEND_INDEX): + return FileResponse(FRONTEND_INDEX) + logger.warning( + "Frontend index '%s' not found; returning JSON status instead.", + FRONTEND_INDEX, + ) return { "name": settings.APP_NAME, "version": settings.APP_VERSION, "status": "running", + "detail": "Frontend assets not found; API is running but UI is unavailable.", } diff --git a/src/backend/init_admin_user.py b/src/backend/init_admin_user.py new file mode 100644 index 0000000..69dfea9 --- /dev/null +++ b/src/backend/init_admin_user.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Initialize the default admin user in the database. + +This mirrors scripts/create_admin.py but is designed to be safe +to run multiple times without resetting existing credentials. +""" + +import os + +from app.core.database import SessionLocal +from app.models import User, UserRole +from app.core.security import get_password_hash + + +def main() -> None: + admin_username = os.getenv("ADMIN_USERNAME", "admin") + admin_password = os.getenv("ADMIN_PASSWORD", "admin") + admin_email = os.getenv("ADMIN_EMAIL", f"{admin_username}@localhost") + + db = SessionLocal() + try: + admin = ( + db.query(User) + .filter(User.username == admin_username) + .first() + ) + if admin: + print( + f"\u2713 Admin user '{admin_username}' already exists; " + "leaving credentials unchanged" + ) + return + + hashed_password = get_password_hash(admin_password) + admin = User( + username=admin_username, + email=admin_email, + hashed_password=hashed_password, + role=UserRole.ADMIN, + is_active=True, + totp_enabled=False, + totp_secret=None, + ) + db.add(admin) + db.commit() + db.refresh(admin) + print(f"\u2713 Created default admin user '{admin_username}'") + except Exception as exc: # pragma: no cover - init helper + db.rollback() + print(f"\u2717 Failed to initialize admin user: {exc}") + raise + finally: + db.close() + + +if __name__ == "__main__": + main() + diff --git a/src/backend/init_system_settings.py b/src/backend/init_system_settings.py new file mode 100644 index 0000000..3900782 --- /dev/null +++ b/src/backend/init_system_settings.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Initialize core system settings in the database. + +This mirrors the behaviour of the installer, making sure that +app_name and app_version keys exist in the system_settings table. + +The script is safe to run multiple times (idempotent). +""" + +import os +from typing import Optional + +from app.core.database import SessionLocal +from app.models.database import SystemSettings + + +def upsert_setting(db, key: str, value: str, description: Optional[str] = None) -> None: + """Insert or update a single setting row by key.""" + setting = db.query(SystemSettings).filter(SystemSettings.key == key).first() + if setting: + setting.value = value + if description is not None: + setting.description = description + else: + setting = SystemSettings(key=key, value=value, description=description) + db.add(setting) + + +def main() -> None: + app_version = os.getenv("APP_VERSION", "1.1.9") + + db = SessionLocal() + try: + upsert_setting( + db, + "app_version", + app_version, + "Current application version", + ) + upsert_setting( + db, + "app_name", + "Depl0y", + "Application name", + ) + db.commit() + print("\u2713 System settings initialized/updated") + except Exception as exc: # pragma: no cover - init helper + db.rollback() + print(f"\u2717 Failed to initialize system settings: {exc}") + raise + finally: + db.close() + + +if __name__ == "__main__": + main() +