2026-03-02 11:34:56 -08:00
#!/usr/bin/env bash
# Create LXC 7805 (sankofa-studio): FusionAI Creator stack (API + worker + services) for Sankofa Studio at https://studio.sankofa.nexus
# Usage: ./scripts/deployment/deploy-sankofa-studio-lxc.sh [--dry-run] [--skip-create]
# --dry-run Print commands only.
# --skip-create Use existing container 7805 (only install Docker / compose / deploy app).
# Env: PROXMOX_HOST, NODE, VMID, HOSTNAME, IP_SANKOFA_STUDIO, REPO_URL or REPO_PATH, ENV_FILE.
# See: docs/03-deployment/SANKOFA_STUDIO_DEPLOYMENT.md
set -euo pipefail
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
PROXMOX_ROOT = " $( cd " $SCRIPT_DIR /../.. " && pwd ) "
IP_CONFIG_PATH = " ${ IP_CONFIG_PATH :- } "
if [ [ -n " $IP_CONFIG_PATH " && -f " $IP_CONFIG_PATH " ] ] ; then
source " $IP_CONFIG_PATH " 2>/dev/null || true
elif [ [ -f " $PROXMOX_ROOT /config/ip-addresses.conf " ] ] ; then
source " $PROXMOX_ROOT /config/ip-addresses.conf " 2>/dev/null || true
elif [ [ -f " $SCRIPT_DIR /../../config/ip-addresses.conf " ] ] ; then
source " $SCRIPT_DIR /../../config/ip-addresses.conf " 2>/dev/null || true
fi
VMID = " ${ VMID :- ${ SANKOFA_STUDIO_VMID :- 7805 } } "
HOSTNAME = " ${ HOSTNAME :- sankofa -studio } "
IP = " ${ IP_SANKOFA_STUDIO :- 192 .168.11.72 } "
GATEWAY = " ${ NETWORK_GATEWAY :- 192 .168.11.1 } "
NETWORK = " ${ NETWORK :- vmbr0 } "
STORAGE = " ${ STORAGE :- local -lvm } "
TEMPLATE = " ${ TEMPLATE :- local : vztmpl /ubuntu-22.04-standard_22.04-1_amd64.tar.zst } "
MEMORY_MB = " ${ MEMORY_MB :- 8192 } "
CORES = " ${ CORES :- 4 } "
DISK_GB = " ${ DISK_GB :- 60 } "
REPO_URL = " ${ REPO_URL :- } "
REPO_PATH = " ${ REPO_PATH :- } "
ENV_FILE = " ${ ENV_FILE :- } "
APP_DIR = " ${ APP_DIR :- /srv/fusionai-creator } "
PROXMOX_HOST = " ${ PROXMOX_HOST :- } "
NODE = " ${ NODE :- } "
SSH_OPTS = "-o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new"
DRY_RUN = false
SKIP_CREATE = false
for a in " $@ " ; do
[ [ " $a " = = "--dry-run" ] ] && DRY_RUN = true
[ [ " $a " = = "--skip-create" ] ] && SKIP_CREATE = true
done
run_cmd( ) {
if [ [ -n " $PROXMOX_HOST " ] ] ; then
ssh $SSH_OPTS root@" $PROXMOX_HOST " " $@ "
else
bash -c " $* "
fi
}
run_pct( ) {
local node_opt = ""
[ [ -n " $NODE " && -z " $PROXMOX_HOST " ] ] && node_opt = " --node $NODE "
if [ [ -n " $PROXMOX_HOST " ] ] ; then
ssh $SSH_OPTS root@" $PROXMOX_HOST " " pct $node_opt $* "
else
pct $node_opt " $@ "
fi
}
pct_exec( ) {
run_pct " exec $VMID -- $* "
}
2026-04-12 06:12:20 -07:00
normalize_studio_sources( ) {
local compose_path = " $APP_DIR /docker-compose.yml "
local app_path = " $APP_DIR /src/fusionai_creator/api/app.py "
pct_exec " bash -lc 'python3 - <<\"PY\"
from pathlib import Path
compose_path = Path( \" $compose_path \" )
if compose_path.exists( ) :
text = compose_path.read_text( )
updated = text.replace( \" version: \\ \" 3.9\\ \" \\ n\\ n\" , \" \" , 1)
updated = updated.replace(
\" test: [ \\ \" CMD\\ \" , \\ \" curl\\ \" , \\ \" -f\\ \" , \\ \" http://localhost:8000/health\\ \" ] \" ,
\" test: [ \\ \" CMD\\ \" , \\ \" python3\\ \" , \\ \" -c\\ \" , \\ \" import urllib.request; urllib.request.urlopen( \\ \\ \\ \" http://localhost:8000/health\\ \\ \\ \" ) .read( ) \\ \" ] \" ,
1,
)
if updated != text:
compose_path.write_text( updated)
app_path = Path( \" $app_path \" )
if app_path.exists( ) :
text = app_path.read_text( )
updated = text
import_line = \" from fastapi.responses import RedirectResponse\\ n\"
anchor = \" from fastapi.staticfiles import StaticFiles\\ n\"
if import_line not in updated and anchor in updated:
updated = updated.replace( anchor, anchor + import_line, 1)
route_block = (
\" @app.api_route( \\ \" /\\ \" , methods = [ \\ \" GET\\ \" , \\ \" HEAD\\ \" ] , include_in_schema = False) \\ n\"
\" def studio_root( ) :\\ n\"
\" return RedirectResponse( url = \\ \" /studio/\\ \" , status_code = 302) \\ n\\ n\\ n\"
)
legacy_route = \" @app.get( \\ \" /\\ \" , include_in_schema = False) \\ ndef studio_root( ) :\"
route_anchor = \" @app.post( \\ \" /jobs\\ \" , dependencies = [ Depends( _verify_api_key) , Depends( _rate_limit) ] ) \\ n\"
if legacy_route in updated:
updated = updated.replace(
legacy_route,
\" @app.api_route( \\ \\ \\ \" /\\ \\ \\ \" , methods = [ \\ \\ \\ \" GET\\ \\ \\ \" , \\ \\ \\ \" HEAD\\ \\ \\ \" ] , include_in_schema = False) \\ ndef studio_root( ) :\" ,
1,
)
elif \" def studio_root( ) \" not in updated and route_anchor in updated:
updated = updated.replace( route_anchor, route_block + route_anchor, 1)
if updated != text:
app_path.write_text( updated)
PY' "
}
2026-03-02 11:34:56 -08:00
echo " === Sankofa Studio LXC ( $VMID ) — $HOSTNAME === "
echo " URL: https://studio.sankofa.nexus → http:// ${ IP } :8000 "
echo " IP: $IP | Memory: ${ MEMORY_MB } MB | Cores: $CORES | Disk: ${ DISK_GB } G "
echo ""
# pct runs only on Proxmox hosts; from another machine set PROXMOX_HOST to SSH there
if ! $DRY_RUN && [ [ -z " ${ PROXMOX_HOST :- } " ] ] && ! command -v pct & >/dev/null; then
echo "ERROR: 'pct' not found. This script must run on a Proxmox host or with PROXMOX_HOST set."
echo ""
echo "From your current machine, run:"
echo " PROXMOX_HOST=192.168.11.11 REPO_URL='https://gitea.d-bis.org/d-bis/FusionAI-Creator.git' $0 "
echo ""
echo "Or SSH to the Proxmox host and run the script there (with REPO_URL set)."
exit 1
fi
if ! $SKIP_CREATE ; then
if $DRY_RUN ; then
echo " [DRY-RUN] Would create LXC $VMID with hostname= $HOSTNAME , ip= $IP /24 (Docker + FusionAI Creator) "
exit 0
fi
if run_pct list 2>/dev/null | grep -q " $VMID " ; then
echo " Container $VMID already exists. Use --skip-create to only install/deploy app. "
exit 0
fi
echo " Creating CT $VMID ( $HOSTNAME )... "
node_opt = ""
[ [ -n " $NODE " && -z " $PROXMOX_HOST " ] ] && node_opt = " --node $NODE "
run_cmd " pct create $VMID $TEMPLATE \
--hostname $HOSTNAME \
--memory $MEMORY_MB \
--cores $CORES \
--rootfs $STORAGE :${ DISK_GB } \
--net0 name = eth0,bridge= $NETWORK ,ip= $IP /24,gw= $GATEWAY \
--nameserver ${ DNS_PRIMARY :- 1 .1.1.1 } \
--description 'Sankofa Studio (FusionAI Creator) - studio.sankofa.nexus. See docs/03-deployment/SANKOFA_STUDIO_DEPLOYMENT.md' \
--start 1 \
--onboot 1 \
--unprivileged 0 \
--features nesting = 1 \
$node_opt "
echo "Waiting for container to boot..."
sleep 25
fi
if $DRY_RUN ; then
echo "[DRY-RUN] Would install Docker, clone/copy app, set .env, docker-compose up -d"
exit 0
fi
echo "Installing Docker and Docker Compose..."
pct_exec "bash -c 'export DEBIAN_FRONTEND=noninteractive && apt-get update -qq && apt-get install -y -qq ca-certificates curl gnupg'"
# Docker repo: source os-release in same shell so \$ID and \$VERSION_CODENAME are set
pct_exec "bash -c 'source /etc/os-release; install -m 0755 -d /etc/apt/keyrings; curl -fsSL \"https://download.docker.com/linux/\$ID/gpg\" | gpg --batch --dearmor -o /etc/apt/keyrings/docker.gpg; chmod a+r /etc/apt/keyrings/docker.gpg'"
pct_exec "bash -c 'source /etc/os-release; echo \"deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/\$ID \$VERSION_CODENAME stable\" | tee /etc/apt/sources.list.d/docker.list > /dev/null'"
pct_exec "bash -c 'apt-get update -qq && apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin git'"
pct_exec "systemctl enable docker && systemctl start docker"
if [ [ -z " $REPO_URL " && -z " $REPO_PATH " ] ] ; then
echo " REPO_URL or REPO_PATH not set. Skipping clone/copy. Create $APP_DIR and add docker-compose + .env manually, then run: docker compose -f $APP_DIR /docker-compose.yml up -d "
exit 0
fi
pct_exec " mkdir -p $( dirname " $APP_DIR " ) "
if [ [ -n " $REPO_PATH " && -d " $REPO_PATH " ] ] ; then
echo " Copying repo from $REPO_PATH into container... "
run_pct " push $VMID $REPO_PATH $APP_DIR "
elif [ [ -n " $REPO_URL " ] ] ; then
echo " Cloning $REPO_URL into container... "
pct_exec " bash -c 'git clone --depth 1 \" $REPO_URL \" \" $APP_DIR \"' "
fi
if [ [ -n " $ENV_FILE " && -f " $ENV_FILE " ] ] ; then
echo " Pushing .env from $ENV_FILE ... "
run_pct " push $VMID $ENV_FILE $APP_DIR /.env "
fi
2026-04-12 06:12:20 -07:00
echo "Normalizing FusionAI Creator app and compose files for clean Studio routing and health checks..."
normalize_studio_sources
2026-03-02 11:34:56 -08:00
echo "Starting FusionAI Creator stack (docker compose up -d)..."
pct_exec " bash -c 'cd \" $APP_DIR \" && docker compose up -d' "
2026-04-12 06:12:20 -07:00
echo "Setting FusionAI Creator containers to restart automatically after Docker/container restarts..."
pct_exec " bash -c 'cd \" $APP_DIR \" && docker compose ps -q | xargs -r docker update --restart unless-stopped > /dev/null' "
2026-03-02 11:34:56 -08:00
echo ""
echo " Done. Verify: curl -s http:// ${ IP } :8000/health "
echo " Studio UI: http:// ${ IP } :8000/studio/ "
echo " Configure NPMplus: studio.sankofa.nexus -> http:// ${ IP } :8000 (see SANKOFA_STUDIO_DEPLOYMENT.md) "