feat(docker): add split backend/frontend stacks and dynamic versioning

This commit is contained in:
Alphaeus Mote
2025-11-19 09:13:21 -05:00
parent be6d358086
commit f294047c87
19 changed files with 696 additions and 8 deletions
+34
View File
@@ -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###
+61
View File
@@ -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"]
@@ -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."
@@ -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}
@@ -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}"
+37
View File
@@ -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
+64
View File
@@ -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"]
@@ -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."
@@ -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}
@@ -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}"
+18
View File
@@ -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###
+16
View File
@@ -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;"]
@@ -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."
@@ -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}
+12
View File
@@ -0,0 +1,12 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
}
}
+12 -2
View File
@@ -5,7 +5,13 @@ import os
def get_app_version(): 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: try:
import sqlite3 import sqlite3
db_path = os.getenv("DATABASE_URL", "sqlite:////var/lib/depl0y/db/depl0y.db") db_path = os.getenv("DATABASE_URL", "sqlite:////var/lib/depl0y/db/depl0y.db")
@@ -21,10 +27,11 @@ def get_app_version():
if result: if result:
return result[0] return result[0]
except Exception as e: except Exception:
# Fallback to hardcoded version if database query fails # Fallback to hardcoded version if database query fails
pass pass
# 3) Final fallback to baked-in default
return "1.1.9" return "1.1.9"
@@ -93,6 +100,9 @@ class Settings(BaseSettings):
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO") LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
LOG_FILE: str = os.getenv("LOG_FILE", "/var/log/depl0y/app.log") 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: class Config:
case_sensitive = True case_sensitive = True
env_file = ".env" env_file = ".env"
+32 -3
View File
@@ -2,7 +2,8 @@
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError 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.config import settings
from app.core.database import init_db 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 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=["*"], 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 # Add validation error handler for debugging
@app.exception_handler(RequestValidationError) @app.exception_handler(RequestValidationError)
@@ -65,13 +87,20 @@ async def startup_event():
logger.info("Database initialized") logger.info("Database initialized")
@app.get("/") @app.get("/", include_in_schema=False, response_class=FileResponse)
async def root(): 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 { return {
"name": settings.APP_NAME, "name": settings.APP_NAME,
"version": settings.APP_VERSION, "version": settings.APP_VERSION,
"status": "running", "status": "running",
"detail": "Frontend assets not found; API is running but UI is unavailable.",
} }
+58
View File
@@ -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()
+58
View File
@@ -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()