fix: docker frontend + docs cleanup

This commit is contained in:
James Pine
2026-03-16 05:28:05 -07:00
parent 3e4d9ff641
commit 4a8a9eac14
13 changed files with 295 additions and 23 deletions
+38
View File
@@ -77,6 +77,7 @@ def create_app() -> FastAPI:
_configure_cors(application)
register_routers(application)
_register_lifecycle(application)
_mount_frontend(application)
return application
@@ -104,6 +105,43 @@ def _configure_cors(application: FastAPI) -> None:
)
def _mount_frontend(application: FastAPI) -> None:
"""Serve the built web frontend when present (Docker / web deployment).
The Dockerfile copies the Vite build output to ``/app/frontend/``. When
that directory exists we mount static assets and add a catch-all route so
the React SPA handles client-side routing. In dev or API-only mode the
directory is absent and this function is a no-op.
"""
frontend_dir = Path(__file__).resolve().parent.parent / "frontend"
if not frontend_dir.is_dir():
return
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
# Mount hashed assets (JS, CSS, images) that Vite places under /assets
assets_dir = frontend_dir / "assets"
if assets_dir.is_dir():
application.mount(
"/assets",
StaticFiles(directory=str(assets_dir)),
name="frontend-assets",
)
# SPA catch-all: serve files if they exist, otherwise index.html for
# client-side routes like /voices, /stories, /models, etc.
@application.get("/{full_path:path}")
async def serve_spa(full_path: str):
file_path = (frontend_dir / full_path).resolve()
# Guard against path traversal — only serve files inside frontend_dir
if full_path and file_path.is_file() and str(file_path).startswith(str(frontend_dir)):
return FileResponse(file_path)
return FileResponse(frontend_dir / "index.html", media_type="text/html")
logger.info("Frontend: serving SPA from %s", frontend_dir)
def _get_gpu_status() -> str:
"""Return a human-readable string describing GPU availability."""
backend_type = get_backend_type()
+9 -1
View File
@@ -3,9 +3,11 @@
import asyncio
import os
import signal
from pathlib import Path
import torch
from fastapi import APIRouter, Depends
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from .. import config, models
@@ -15,12 +17,18 @@ from ..utils.platform_detect import get_backend_type
router = APIRouter()
# Frontend build directory — present in Docker, absent in dev/API-only mode
_frontend_dir = Path(__file__).resolve().parent.parent.parent / "frontend"
@router.get("/")
async def root():
"""Root endpoint."""
"""Root endpoint — serves SPA index.html in Docker, JSON otherwise."""
from .. import __version__
index = _frontend_dir / "index.html"
if index.is_file():
return FileResponse(index, media_type="text/html")
return {"message": "voicebox API", "version": __version__}
+1 -5
View File
@@ -6,11 +6,7 @@ This is a Next.js application generated with
Run development server:
```bash
npm run dev
# or
pnpm dev
# or
yarn dev
bun run dev
```
Open http://localhost:3000 with your browser to see the result.
-6
View File
@@ -1,6 +0,0 @@
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: LayoutProps<'/'>) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
}
-5
View File
@@ -1,5 +0,0 @@
import { redirect } from 'next/navigation';
export default function HomePage() {
redirect('/docs');
}
@@ -1,8 +1,8 @@
import { source } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
import { source } from '@/lib/source';
export default function Layout({ children }: LayoutProps<'/docs'>) {
export default function Layout({ children }: LayoutProps<'/[[...slug]]'>) {
return (
<DocsLayout tree={source.pageTree} {...baseOptions()}>
{children}
@@ -7,7 +7,7 @@ import { APIPage } from '@/components/api-page';
import { getPageImage, source } from '@/lib/source';
import { getMDXComponents } from '@/mdx-components';
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
export default async function Page(props: PageProps<'/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
@@ -59,7 +59,7 @@ export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
export async function generateMetadata(props: PageProps<'/[[...slug]]'>): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
+240
View File
@@ -0,0 +1,240 @@
---
title: "Docker Deployment"
description: "Run Voicebox as a headless server with a web UI using Docker"
---
## Overview
Voicebox can run as a Docker container with a full web UI -- no desktop app required. This is ideal for headless servers, shared GPU machines, or self-hosted deployments.
## Quick Start
```bash
git clone https://github.com/jamiepine/voicebox.git
cd voicebox
docker compose up
```
Open [http://localhost:17493](http://localhost:17493) in your browser. The full Voicebox UI is served directly from the backend.
<Callout type="info">
The first build takes a few minutes (compiling the frontend, installing Python dependencies). Subsequent starts are fast thanks to Docker layer caching.
</Callout>
## How It Works
The Docker image uses a 3-stage build:
1. **Frontend** -- builds the React SPA with Bun and Vite
2. **Backend** -- installs Python dependencies and TTS model packages
3. **Runtime** -- combines both into a minimal image running the FastAPI server
The backend serves the web UI automatically when the built frontend is present. All API routes work exactly as they do in the desktop app.
## Configuration
### docker-compose.yml
The default `docker-compose.yml` binds to localhost only, mounts persistent volumes for data and model cache, and sets sensible resource limits:
```yaml
services:
voicebox:
build: .
container_name: voicebox
restart: unless-stopped
ports:
- "127.0.0.1:17493:17493"
volumes:
- ./output:/app/data/generations
- voicebox-data:/app/data
- huggingface-cache:/home/voicebox/.cache/huggingface
environment:
- LOG_LEVEL=info
deploy:
resources:
limits:
cpus: '4'
memory: 8G
```
### Exposing to Your Network
By default the container only listens on `127.0.0.1`. To allow other machines on your network to connect, change the port binding:
```yaml
ports:
- "0.0.0.0:17493:17493"
```
<Callout type="warn">
The API has no built-in authentication. Only expose to trusted networks, or put a reverse proxy with auth in front of it.
</Callout>
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `LOG_LEVEL` | `info` | Logging verbosity (`debug`, `info`, `warning`, `error`) |
| `VOICEBOX_MODELS_DIR` | (HuggingFace cache) | Custom path for model storage |
| `VOICEBOX_CORS_ORIGINS` | (local origins) | Additional CORS origins, comma-separated |
### Resource Limits
The default compose file limits the container to 4 CPUs and 8GB RAM. Adjust these based on your hardware:
```yaml
deploy:
resources:
limits:
cpus: '8'
memory: 16G
```
<Callout type="info">
TTS model inference is memory-intensive. 8GB is the minimum for running a single engine. 16GB+ is recommended if you want multiple engines loaded simultaneously.
</Callout>
## Volumes
| Volume | Container Path | Purpose |
|--------|---------------|---------|
| `./output` | `/app/data/generations` | Generated audio files (bind-mount, easy access from host) |
| `voicebox-data` | `/app/data` | Profiles, database, cache |
| `huggingface-cache` | `/home/voicebox/.cache/huggingface` | Downloaded models (persists across rebuilds) |
The `huggingface-cache` volume is important -- without it, models would be re-downloaded every time the container is rebuilt.
## GPU Acceleration
### NVIDIA GPU (CUDA)
To use your NVIDIA GPU inside the container, install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) and add GPU access to your compose file:
```yaml
services:
voicebox:
build: .
# ... existing config ...
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
```
### AMD GPU (ROCm)
For AMD GPUs, use the ROCm runtime:
```yaml
services:
voicebox:
build: .
# ... existing config ...
devices:
- /dev/kfd
- /dev/dri
group_add:
- video
```
### CPU Only
The default configuration runs on CPU. This works fine but generation will be slower. LuxTTS is the fastest engine on CPU (150x realtime).
## Security
The Docker image follows security best practices:
- **Non-root user** -- the server runs as `voicebox`, not `root`
- **Localhost binding** -- only accessible from the host machine by default
- **Health checks** -- automatic restart if the server hangs (`/health` endpoint polled every 30s)
- **CORS restricted** -- only local origins allowed by default
### Running Behind a Reverse Proxy
For production deployments, put Voicebox behind nginx or Caddy with TLS and authentication:
```nginx
server {
listen 443 ssl;
server_name voicebox.example.com;
ssl_certificate /etc/ssl/certs/voicebox.pem;
ssl_certificate_key /etc/ssl/private/voicebox.key;
auth_basic "Voicebox";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
proxy_pass http://127.0.0.1:17493;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
## Troubleshooting
### Container starts but UI shows JSON
If you see `{"message": "voicebox API", ...}` instead of the web UI, the frontend build may have failed during the Docker build. Check the build logs:
```bash
docker compose build --no-cache
```
Look for errors in the "Build frontend" stage.
### Models downloading on every restart
Make sure the `huggingface-cache` volume is configured. Without it, the model cache is lost when the container stops:
```yaml
volumes:
- huggingface-cache:/home/voicebox/.cache/huggingface
```
### Out of memory
TTS models are large. If the container is killed by the OOM killer, increase the memory limit:
```yaml
deploy:
resources:
limits:
memory: 16G
```
### Port already in use
```bash
# Check what's using port 17493
lsof -i :17493
# Or use a different port
ports:
- "127.0.0.1:8080:17493"
```
## Prebuilt Images (Coming Soon)
We plan to publish prebuilt Docker images to GitHub Container Registry so you won't need to build locally:
```bash
# Not available yet — coming in a future release
docker run -p 17493:17493 ghcr.io/jamiepine/voicebox:latest
```
The CPU image will be ~3-4 GB (Python + PyTorch + TTS packages). A separate CUDA tag (~6-8 GB) will be available for NVIDIA GPU users. This is normal for ML containers.
For now, use `docker compose up` to build from source as described above.
## Connecting the Desktop App
You can also use the desktop app as a frontend for a Docker-hosted backend. In the desktop app, go to **Settings -> Server**, enable **Remote Mode**, and enter `http://<server-ip>:17493`.
See the [Remote Mode guide](/overview/remote-mode) for details.
+1
View File
@@ -4,6 +4,7 @@
"pages": [
"introduction",
"installation",
"docker",
"quick-start",
"voice-cloning",
"stories-editor",
+2 -2
View File
@@ -1,10 +1,10 @@
import { docs } from '@/.source';
import { type InferPageType, loader } from 'fumadocs-core/source';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
import { docs } from '@/.source';
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: '/docs',
baseUrl: '/',
source: docs.toFumadocsSource(),
plugins: [lucideIconsPlugin()],
});