Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
output/
docker/dev-secrets/
docker/.env
*.iso
model-staging/
__pycache__/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def _read_version() -> str:
MODELS_DIR = "/var/lib/neuraldrive/models"
DATA_DIR = "/var/lib/neuraldrive"

# When running inside Docker (NEURALDRIVE_CONTAINER=1) there is no systemd,
# journalctl, or hostnamectl. Operations that require them return stub responses.
CONTAINER_MODE = os.environ.get("NEURALDRIVE_CONTAINER") == "1"

ALLOWED_SERVICES = [
"neuraldrive-ollama",
"neuraldrive-webui",
Expand Down Expand Up @@ -98,6 +102,8 @@ def get_system_status():

@app.get("/system/services", dependencies=[Depends(verify_token)])
def list_services():
if CONTAINER_MODE:
return {"services": [{"name": s, "status": "container", "since": ""} for s in ALLOWED_SERVICES]}
results = []
for svc in ALLOWED_SERVICES:
status = _systemctl("is-active", svc)
Expand All @@ -119,6 +125,8 @@ def restart_service(name: str):
raise HTTPException(
status_code=403, detail=f"Service '{name}' not in allowlist"
)
if CONTAINER_MODE:
return {"message": f"Restarted {name} (container mode: use docker compose restart)"}
subprocess.run(["systemctl", "restart", name], check=True)
return {"message": f"Restarted {name}"}

Expand All @@ -131,6 +139,8 @@ def service_action(name: str, action: str):
)
if action not in ("start", "stop"):
raise HTTPException(status_code=400, detail="Action must be 'start' or 'stop'")
if CONTAINER_MODE:
return {"message": f"{action.capitalize()}ed {name} (container mode: use docker compose {action})"}
subprocess.run(["systemctl", action, name], check=True)
return {"message": f"{action.capitalize()}ed {name}"}

Expand All @@ -141,6 +151,8 @@ def get_logs(service: str = "ollama", lines: int = 50, level: str = ""):
service = f"neuraldrive-{service}"
if service not in ALLOWED_SERVICES:
raise HTTPException(status_code=403, detail="Service not in allowlist")
if CONTAINER_MODE:
return {"service": service, "lines": ["(container mode: use `docker compose logs` to view service logs)"]}
capped_lines = min(lines, 500)
cmd = ["journalctl", "-u", service, "-n", str(capped_lines), "--no-pager"]
if level:
Expand Down Expand Up @@ -184,6 +196,8 @@ def get_network():
def set_hostname(hostname: str):
if not hostname or len(hostname) > 63:
raise HTTPException(status_code=400, detail="Hostname must be 1-63 characters")
if CONTAINER_MODE:
return {"message": f"Hostname set to {hostname} (container mode: not persisted)"}
subprocess.run(["hostnamectl", "set-hostname", hostname], check=True)
return {"message": f"Hostname set to {hostname}"}

Expand Down Expand Up @@ -229,6 +243,8 @@ def manage_ssh(action: str):
raise HTTPException(
status_code=400, detail="Action must be 'enable' or 'disable'"
)
if CONTAINER_MODE:
return {"message": f"SSH {action}d (container mode: not applicable)"}
cmd = "start" if action == "enable" else "stop"
subprocess.run(["systemctl", cmd, "ssh"], check=True)
if action == "enable":
Expand All @@ -240,7 +256,7 @@ def manage_ssh(action: str):

@app.get("/system/security", dependencies=[Depends(verify_token)])
def get_security():
ssh_active = _systemctl("is-active", "ssh") == "active"
ssh_active = False if CONTAINER_MODE else _systemctl("is-active", "ssh") == "active"
cert_exists = os.path.exists(f"{CERT_DIR}/server.crt")
cert_expiry = ""
if cert_exists:
Expand Down Expand Up @@ -276,7 +292,8 @@ def rotate_api_key():
Path(API_KEY_PATH).write_text(new_key)
caddy_env_path = Path("/etc/neuraldrive/caddy.env")
caddy_env_path.write_text(f"NEURALDRIVE_API_KEY={new_key}\n")
subprocess.run(["systemctl", "reload", "neuraldrive-caddy"], check=False)
if not CONTAINER_MODE:
subprocess.run(["systemctl", "reload", "neuraldrive-caddy"], check=False)
return {"message": "API key rotated", "new_key": new_key}


Expand Down
81 changes: 81 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
services:
ollama:
image: ollama/ollama:latest
volumes:
- ollama_data:/root/.ollama
networks:
- neuraldrive

ollama-gpu:
image: ollama/ollama:latest
volumes:
- ollama_data:/root/.ollama
networks:
- neuraldrive
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
profiles:
- gpu

webui:
image: ghcr.io/open-webui/open-webui:main
environment:
- OLLAMA_BASE_URL=http://ollama:11434
- WEBUI_NAME=NeuralDrive
- ENABLE_SIGNUP=false
- DEFAULT_USER_ROLE=user
- WEBUI_AUTH=true
- ENABLE_EASTER_EGGS=false
volumes:
- webui_data:/app/backend/data
depends_on:
- ollama
networks:
- neuraldrive

system-api:
build:
context: .
dockerfile: docker/system-api/Dockerfile
environment:
- NEURALDRIVE_CONTAINER=1
env_file:
- docker/.env
volumes:
- ./docker/dev-secrets/api.key:/etc/neuraldrive/api.key:ro
- ./docker/dev-secrets/gpu.conf:/run/neuraldrive/gpu.conf:ro
- ./docker/dev-secrets/tls:/etc/neuraldrive/tls:ro
networks:
- neuraldrive

caddy:
image: caddy:latest
environment:
- NEURALDRIVE_API_KEY=${NEURALDRIVE_API_KEY}
volumes:
- ./docker/Caddyfile.dev:/etc/caddy/Caddyfile:ro
- ./docker/dev-secrets/tls:/etc/neuraldrive/tls:ro
- caddy_data:/data
ports:
- "443:443"
- "8443:8443"
- "11434:11434"
depends_on:
- ollama
- webui
- system-api
networks:
- neuraldrive

networks:
neuraldrive:

volumes:
ollama_data:
webui_data:
caddy_data:
1 change: 1 addition & 0 deletions docker/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEURALDRIVE_API_KEY=change-me
41 changes: 41 additions & 0 deletions docker/Caddyfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
admin off
auto_https disable_redirects
}

:443 {
tls /etc/neuraldrive/tls/server.crt /etc/neuraldrive/tls/server.key

handle /* {
reverse_proxy webui:8080
}
}

:8443 {
tls /etc/neuraldrive/tls/server.crt /etc/neuraldrive/tls/server.key

@api_routes {
path /v1/* /api/*
}
@api_authenticated {
path /v1/* /api/*
header Authorization "Bearer {env.NEURALDRIVE_API_KEY}"
}

handle @api_authenticated {
reverse_proxy ollama:11434
}

handle @api_routes {
respond "Unauthorized — provide Authorization: Bearer <API_KEY>" 401
}

handle /system/* {
uri strip_prefix /system
reverse_proxy system-api:3001
}

handle /health {
respond "OK" 200
}
}
11 changes: 11 additions & 0 deletions docker/system-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM python:3.11-slim

RUN pip install --no-cache-dir fastapi uvicorn psutil httpx

COPY config/includes.chroot/usr/lib/neuraldrive/api/neuraldrive_api /app/neuraldrive_api

WORKDIR /app

ENV NEURALDRIVE_CONTAINER=1

CMD ["uvicorn", "neuraldrive_api.main:app", "--host", "0.0.0.0", "--port", "3001"]
67 changes: 67 additions & 0 deletions scripts/dev-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail

SECRETS_DIR="$(cd "$(dirname "$0")/.." && pwd)/docker/dev-secrets"

echo "Setting up NeuralDrive dev environment..."

mkdir -p "$SECRETS_DIR/tls"

# API key
if [[ ! -f "$SECRETS_DIR/api.key" ]]; then
printf "nd-%s" "$(openssl rand -hex 16)" > "$SECRETS_DIR/api.key"
echo " Generated API key: $SECRETS_DIR/api.key"
else
echo " API key already exists, skipping"
fi

# TLS cert
if [[ ! -f "$SECRETS_DIR/tls/server.crt" ]]; then
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout "$SECRETS_DIR/tls/server.key" \
-out "$SECRETS_DIR/tls/server.crt" \
-days 365 \
-subj "/CN=neuraldrive.local" \
-addext "subjectAltName=DNS:localhost,DNS:neuraldrive.local,IP:127.0.0.1" \
2>/dev/null
echo " Generated TLS cert: $SECRETS_DIR/tls/"
else
echo " TLS cert already exists, skipping"
fi

# GPU stub (CPU-only mode)
if [[ ! -f "$SECRETS_DIR/gpu.conf" ]]; then
printf "GPU_VENDOR=none\nGPU_COUNT=0\n" > "$SECRETS_DIR/gpu.conf"
echo " Created GPU stub (CPU mode): $SECRETS_DIR/gpu.conf"
else
echo " gpu.conf already exists, skipping"
fi

# docker/.env
ENV_FILE="$(cd "$(dirname "$0")/.." && pwd)/docker/.env"
if [[ ! -f "$ENV_FILE" ]]; then
API_KEY=$(cat "$SECRETS_DIR/api.key")
printf "NEURALDRIVE_API_KEY=%s\n" "$API_KEY" > "$ENV_FILE"
echo " Created docker/.env"
else
echo " docker/.env already exists, skipping"
fi

API_KEY=$(cat "$SECRETS_DIR/api.key")

echo ""
echo "Dev environment ready. Start the stack with:"
echo ""
echo " docker compose -f docker-compose.dev.yml up"
echo ""
echo "Endpoints:"
echo " Web UI: https://localhost/"
echo " Health: https://localhost:8443/health"
echo " System API: https://localhost:8443/system/status"
echo " Ollama: http://localhost:11434/api/tags"
echo ""
echo "API key: $API_KEY"
echo "(also in docker/dev-secrets/api.key)"
echo ""
echo "To enable GPU passthrough (NVIDIA + Container Toolkit required):"
echo " docker compose -f docker-compose.dev.yml --profile gpu up"
Loading