From 85df3050796d4582cbbddcaef225f14fd20d8945 Mon Sep 17 00:00:00 2001 From: Daniel Cox Date: Tue, 11 Aug 2026 19:15:10 +0930 Subject: [PATCH 1/8] feat: add Docker support and reverse-proxy compatibility Add Docker infrastructure for running the training WebUI in containers: - Dockerfile based on PyTorch CUDA base image with layer-cached deps - docker-compose.yml with GPU support and nginx reverse proxy - nginx.conf with WebSocket support for Gradio Code fixes for container environments: - Stream training subprocess stdout/stderr to Docker logs - Support GRADIO_ROOT_PATH env var for reverse proxy (nginx/Traefik) - Echo startup URL to stdout for container log discovery All changes are backward-compatible: without Docker or env vars, behavior is identical to before. --- docker/Dockerfile | 51 +++++++++++++++++++ docker/README.md | 104 ++++++++++++++++++++++++++++++++++++++ docker/docker-compose.yml | 34 +++++++++++++ docker/nginx.conf | 21 ++++++++ lora_ft_webui.py | 8 ++- 5 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 docker/Dockerfile create mode 100644 docker/README.md create mode 100644 docker/docker-compose.yml create mode 100644 docker/nginx.conf diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..e22273a --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,51 @@ +# ───────────────────────────────────────────────────────────────────── +# VoxCPM Training WebUI — Docker image +# ───────────────────────────────────────────────────────────────────── +# Base: PyTorch with CUDA for GPU-accelerated LoRA fine-tuning. +# Build context should be the project root: +# +# docker build -f docker/Dockerfile -t voxcpm-training . +# +# ───────────────────────────────────────────────────────────────────── +FROM pytorch/pytorch:2.5.1-cuda12.4-cudnn9-devel + +LABEL maintainer="OpenBMB " +LABEL description="VoxCPM LoRA Training WebUI with GPU support" + +# Avoid interactive prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +# System deps required by Python packages: +# git — setuptools_scm needs it to resolve version in pyproject.toml +# libsndfile1 — C library backing the 'soundfile' Python package +# ffmpeg — audio codec support for torchaudio/librosa +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + libsndfile1 \ + ffmpeg \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Layer 1: Install dependencies only (cached unless pyproject.toml changes) +# Create a minimal package stub so pip can resolve deps without real source. +COPY pyproject.toml /app/ +RUN mkdir -p /app/src/voxcpm && echo '__version__ = "0.0.0"' > /app/src/voxcpm/__init__.py +ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 +RUN pip install --no-cache-dir -e . + +# Layer 2: Copy full project source (cheap rebuild on code changes) +COPY . /app/ + +# Create default directories and declare volumes +RUN mkdir -p /app/lora /app/models /app/output +VOLUME ["/app/models", "/app/lora", "/app/output"] + +EXPOSE 7860 + +# Environment variables for configuration +ENV GRADIO_SERVER_PORT=7860 +ENV GRADIO_ROOT_PATH="" + +# Default: launch training WebUI +CMD ["python", "lora_ft_webui.py"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..dcb4cf3 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,104 @@ +# Docker Support for VoxCPM Training WebUI + +Run the VoxCPM LoRA fine-tuning WebUI in a Docker container with full GPU support and nginx reverse proxy. + +## Prerequisites + +- Docker Engine 19.03+ with [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) +- NVIDIA GPU with CUDA 12.4+ compatible drivers +- At least 16 GB GPU VRAM (24 GB+ recommended for larger models) + +## Quick Start + +```bash +# From the project root directory: +docker compose -f docker/docker-compose.yml up --build +``` + +This starts: +- **training-webui** — the Gradio-based training interface on port 7860 +- **nginx** — reverse proxy serving the WebUI at `http://localhost/webui/` + +Access the WebUI at **http://localhost/webui/**. + +## Direct Access (no proxy) + +If you want to bypass nginx and access Gradio directly: + +```bash +docker compose -f docker/docker-compose.yml up --build training-webui +``` + +Set `GRADIO_ROOT_PATH=` (empty) in the compose file when running without the proxy, then access at `http://localhost:7860`. + +## Building Manually + +```bash +# Build the image +docker build -f docker/Dockerfile -t voxcpm-training . + +# Run with GPU access (no reverse proxy) +docker run --gpus all -p 7860:7860 \ + -v ./models:/app/models \ + -v ./lora:/app/lora \ + -v ./output:/app/output \ + voxcpm-training +``` + +## Model Weights + +Models are **auto-downloaded** from HuggingFace Hub on first use. The `/app/models` volume persists them across container restarts so they don't need to be re-downloaded. + +To pre-populate (avoids download at startup): + +``` +models/ +├── openbmb__VoxCPM2/ # VoxCPM2 (preferred) +└── openbmb__VoxCPM1.5/ # VoxCPM1.5 (fallback) +``` + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `GRADIO_SERVER_PORT` | `7860` | Port for the WebUI server | +| `GRADIO_ROOT_PATH` | `""` | URL prefix when behind a reverse proxy (e.g., `/webui`) | + +## Reverse Proxy + +The included `docker-compose.yml` ships with an nginx reverse proxy that serves the WebUI at `/webui/`. The `GRADIO_ROOT_PATH=/webui` env var ensures Gradio generates correct URLs for assets and WebSocket connections. + +### Custom nginx config + +Edit `docker/nginx.conf` to change the location prefix or add TLS. + +### Traefik Example (labels) + +```yaml +labels: + - "traefik.http.routers.voxcpm.rule=PathPrefix(`/webui`)" + - "traefik.http.services.voxcpm.loadbalancer.server.port=7860" +``` + +## Viewing Training Logs + +Training subprocess output is streamed to stdout, visible via: + +```bash +docker compose -f docker/docker-compose.yml logs -f training-webui +``` + +## Volumes + +| Mount Point | Purpose | +|-------------|---------| +| `/app/models` | Pre-trained model weights (read-only OK) | +| `/app/lora` | LoRA checkpoints — training output is saved here | +| `/app/output` | Additional training artifacts | + +## Troubleshooting + +- **"no NVIDIA GPU detected"**: Ensure the NVIDIA Container Toolkit is installed and `docker run --gpus all nvidia-smi` works. +- **OOM errors**: Reduce batch size in the WebUI or use a GPU with more VRAM. +- **WebUI not accessible**: Check that port 80 (nginx) or 7860 (direct) isn't blocked by a firewall. +- **WebSocket errors behind proxy**: Ensure your proxy forwards `Upgrade` and `Connection` headers (the included nginx.conf handles this). diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..8dbf9b8 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,34 @@ +version: "3.8" + +services: + training-webui: + build: + context: .. + dockerfile: docker/Dockerfile + ports: + - "7860:7860" + volumes: + - ../models:/app/models # Pre-downloaded model weights + - ../lora:/app/lora # LoRA checkpoints (input/output) + - ../output:/app/output # Training output artifacts + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + environment: + - GRADIO_SERVER_PORT=7860 + - GRADIO_ROOT_PATH=/webui # Matches nginx location block + restart: unless-stopped + + nginx: + image: nginx:alpine + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - training-webui + restart: unless-stopped diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..0ac18c6 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 80; + server_name _; + + location /webui/ { + proxy_pass http://training-webui:7860/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support (required for Gradio) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Increase timeouts for long-running training operations + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } +} diff --git a/lora_ft_webui.py b/lora_ft_webui.py index 3d91c3d..47a4079 100644 --- a/lora_ft_webui.py +++ b/lora_ft_webui.py @@ -500,6 +500,7 @@ def start_training( assert training_process.stdout is not None for line in training_process.stdout: + print(line, end="", flush=True) # Stream to stdout (Docker logs) training_log += line # Keep log size manageable if len(training_log) > 100000: @@ -1324,4 +1325,9 @@ with gr.Blocks(title="VoxCPM LoRA WebUI", theme=gr.themes.Soft(), css=custom_css if __name__ == "__main__": # Ensure lora directory exists os.makedirs("lora", exist_ok=True) - app.queue().launch(server_name="0.0.0.0", server_port=7860) + + port = int(os.environ.get("GRADIO_SERVER_PORT", "7860")) + root_path = os.environ.get("GRADIO_ROOT_PATH", "") + + print(f"\U0001f399\ufe0f VoxCPM Training WebUI: http://0.0.0.0:{port}{root_path}", flush=True) + app.queue().launch(server_name="0.0.0.0", server_port=port, root_path=root_path) From da5b2da097103bbf922165d48999dfcd6e4e9c40 Mon Sep 17 00:00:00 2001 From: Daniel Cox Date: Tue, 11 Aug 2026 21:46:24 +0930 Subject: [PATCH 2/8] fix: add health check + clarify volume mounts - nginx: return 200 OK on GET / for load balancer health checks - Dockerfile: set HF_HOME=/app/models so Hub downloads persist in mounted volume - Dockerfile: add /app/data directory and volume declaration - docker-compose: explicit volume mounts for models, data, lora, output - README: document where to put training files and find output - .gitignore: exclude volume mount directories (models/, data/, lora/, output/) --- .gitignore | 6 +++ docker/Dockerfile | 5 ++- docker/README.md | 91 ++++++++++++++++++++++++++++++--------- docker/docker-compose.yml | 17 ++++++-- docker/nginx.conf | 6 +++ 5 files changed, 100 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index f7fa981..61bac30 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,9 @@ voxcpm.egg-info .DS_Store ./pretrained_models/ app_local.py + +# Docker volume mount directories (large files, user-specific) +models/ +data/ +lora/ +output/ diff --git a/docker/Dockerfile b/docker/Dockerfile index e22273a..b37d20c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,14 +38,15 @@ RUN pip install --no-cache-dir -e . COPY . /app/ # Create default directories and declare volumes -RUN mkdir -p /app/lora /app/models /app/output -VOLUME ["/app/models", "/app/lora", "/app/output"] +RUN mkdir -p /app/lora /app/models /app/output /app/data +VOLUME ["/app/models", "/app/lora", "/app/output", "/app/data"] EXPOSE 7860 # Environment variables for configuration ENV GRADIO_SERVER_PORT=7860 ENV GRADIO_ROOT_PATH="" +ENV HF_HOME=/app/models # Default: launch training WebUI CMD ["python", "lora_ft_webui.py"] diff --git a/docker/README.md b/docker/README.md index dcb4cf3..51609fb 100644 --- a/docker/README.md +++ b/docker/README.md @@ -21,6 +21,75 @@ This starts: Access the WebUI at **http://localhost/webui/**. +## Volume Mounts + +The compose file maps host directories to container paths. Create these directories at the project root before starting: + +``` +VoxCPM/ +├── docker/ +│ ├── docker-compose.yml +│ ├── Dockerfile +│ └── nginx.conf +├── models/ ← Pretrained model weights (or auto-downloaded via HF) +│ ├── openbmb__VoxCPM2/ +│ └── openbmb__VoxCPM1.5/ +├── data/ ← Training manifests + audio files +│ ├── train.jsonl +│ ├── val.jsonl (optional) +│ └── audio/ +│ ├── speaker1_001.wav +│ └── ... +├── lora/ ← LoRA training output (created automatically) +│ └── my-voice-2024/ +│ ├── checkpoints/ +│ ├── logs/ +│ └── train_config.yaml +└── output/ ← Additional training artifacts +``` + +### Mount Reference + +| Host Path | Container Path | Purpose | +|-----------|---------------|---------| +| `./models/` | `/app/models` | Pretrained model weights and HF cache (`HF_HOME`). Pre-populate with model dirs (e.g., `openbmb__VoxCPM2/`) or leave empty — models auto-download on first run and persist here. | +| `./data/` | `/app/data` | Training data. Put JSONL manifests and audio files here. In the WebUI, reference paths as `/app/data/train.jsonl`. | +| `./lora/` | `/app/lora` | LoRA checkpoint output. After training, find results in `lora//checkpoints/`. Also used to resume training from existing checkpoints. | +| `./output/` | `/app/output` | Miscellaneous training artifacts. | + +### Training Data Format + +The train manifest is a JSONL file where each line references an audio file: + +```json +{"audio_path": "/app/data/audio/speaker1_001.wav", "text": "Hello world", "speaker": "speaker1"} +``` + +Use absolute container paths (`/app/data/...`) in your manifest so the container can find the files. + +### Models + +If `models/openbmb__VoxCPM2/` exists on the host, the app loads directly from that path — no network access needed. If the directory is empty or missing, `from_pretrained` falls back to `snapshot_download` from HuggingFace Hub. + +The Dockerfile sets `HF_HOME=/app/models` so any Hub downloads land in the same mounted volume (matching the pattern in `deploy/Dockerfile.voxcpm-unified`). This means models persist across container restarts regardless of whether they were pre-populated or auto-downloaded. + +**Recommended:** Pre-populate to avoid first-run download delay: + +```bash +huggingface-cli download openbmb/VoxCPM2 --local-dir ./models/openbmb__VoxCPM2 +``` + +The Dockerfile creates empty `/app/models`, `/app/lora`, `/app/output` directories, but the volume mounts override them with your host directories. + +## Health Check + +The nginx proxy responds with `200 OK` on `GET /` for load balancer health checks (AWS ALB, etc.). This is separate from the WebUI at `/webui/`. + +```bash +curl http://localhost/ +# OK +``` + ## Direct Access (no proxy) If you want to bypass nginx and access Gradio directly: @@ -40,23 +109,12 @@ docker build -f docker/Dockerfile -t voxcpm-training . # Run with GPU access (no reverse proxy) docker run --gpus all -p 7860:7860 \ -v ./models:/app/models \ + -v ./data:/app/data \ -v ./lora:/app/lora \ -v ./output:/app/output \ voxcpm-training ``` -## Model Weights - -Models are **auto-downloaded** from HuggingFace Hub on first use. The `/app/models` volume persists them across container restarts so they don't need to be re-downloaded. - -To pre-populate (avoids download at startup): - -``` -models/ -├── openbmb__VoxCPM2/ # VoxCPM2 (preferred) -└── openbmb__VoxCPM1.5/ # VoxCPM1.5 (fallback) -``` - ## Environment Variables | Variable | Default | Description | @@ -88,17 +146,10 @@ Training subprocess output is streamed to stdout, visible via: docker compose -f docker/docker-compose.yml logs -f training-webui ``` -## Volumes - -| Mount Point | Purpose | -|-------------|---------| -| `/app/models` | Pre-trained model weights (read-only OK) | -| `/app/lora` | LoRA checkpoints — training output is saved here | -| `/app/output` | Additional training artifacts | - ## Troubleshooting - **"no NVIDIA GPU detected"**: Ensure the NVIDIA Container Toolkit is installed and `docker run --gpus all nvidia-smi` works. - **OOM errors**: Reduce batch size in the WebUI or use a GPU with more VRAM. - **WebUI not accessible**: Check that port 80 (nginx) or 7860 (direct) isn't blocked by a firewall. - **WebSocket errors behind proxy**: Ensure your proxy forwards `Upgrade` and `Connection` headers (the included nginx.conf handles this). +- **Health check failing**: Ensure nginx is running — `curl http://localhost/` should return `OK`. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8dbf9b8..79621ab 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -8,9 +8,20 @@ services: ports: - "7860:7860" volumes: - - ../models:/app/models # Pre-downloaded model weights - - ../lora:/app/lora # LoRA checkpoints (input/output) - - ../output:/app/output # Training output artifacts + # Pretrained model weights + HF cache (HF_HOME=/app/models in Dockerfile). + # Pre-populate with model dirs, or leave empty — auto-downloads on first run. + - ../models:/app/models + + # Training data: JSONL manifests and audio files. + # Reference paths inside the container as /app/data/train.jsonl etc. + - ../data:/app/data + + # LoRA training output — checkpoints, configs, logs. + # Results appear in lora//checkpoints/ after training. + - ../lora:/app/lora + + # Additional training artifacts. + - ../output:/app/output deploy: resources: reservations: diff --git a/docker/nginx.conf b/docker/nginx.conf index 0ac18c6..5e5ea01 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -2,6 +2,12 @@ server { listen 80; server_name _; + # Health check for load balancers (AWS ALB, etc.) + location = / { + return 200 'OK\n'; + add_header Content-Type text/plain; + } + location /webui/ { proxy_pass http://training-webui:7860/; proxy_set_header Host $host; From 74b98d2c1ee2a7ee1ac5e32355354addc5a28754 Mon Sep 17 00:00:00 2001 From: Daniel Cox Date: Wed, 12 Aug 2026 09:00:55 +0930 Subject: [PATCH 3/8] fix: trust forwarded headers for HTTPS file URLs Add FORWARDED_ALLOW_IPS=* so uvicorn/Gradio trusts X-Forwarded-Proto from nginx in Docker (bridge network 172.x.x.x). Without this, Gradio generates http:// file URLs behind HTTPS proxies, causing mixed-content errors that block audio playback. --- docker/README.md | 2 ++ docker/docker-compose.yml | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/docker/README.md b/docker/README.md index 51609fb..40570b3 100644 --- a/docker/README.md +++ b/docker/README.md @@ -121,6 +121,7 @@ docker run --gpus all -p 7860:7860 \ |----------|---------|-------------| | `GRADIO_SERVER_PORT` | `7860` | Port for the WebUI server | | `GRADIO_ROOT_PATH` | `""` | URL prefix when behind a reverse proxy (e.g., `/webui`) | +| `FORWARDED_ALLOW_IPS` | `*` | IPs allowed to set `X-Forwarded-Proto`. Set to `*` when behind a Docker reverse proxy so Gradio generates `https://` file URLs. | ## Reverse Proxy @@ -153,3 +154,4 @@ docker compose -f docker/docker-compose.yml logs -f training-webui - **WebUI not accessible**: Check that port 80 (nginx) or 7860 (direct) isn't blocked by a firewall. - **WebSocket errors behind proxy**: Ensure your proxy forwards `Upgrade` and `Connection` headers (the included nginx.conf handles this). - **Health check failing**: Ensure nginx is running — `curl http://localhost/` should return `OK`. +- **Mixed-content / audio not playing over HTTPS**: Gradio generates `http://` file URLs because uvicorn doesn't trust the `X-Forwarded-Proto` header from the Docker bridge network. The compose file sets `FORWARDED_ALLOW_IPS=*` to fix this. If you run without compose, pass `-e FORWARDED_ALLOW_IPS=*` to `docker run`. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 79621ab..8fea42d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -32,6 +32,11 @@ services: environment: - GRADIO_SERVER_PORT=7860 - GRADIO_ROOT_PATH=/webui # Matches nginx location block + # Tell uvicorn to trust X-Forwarded-Proto from any IP (nginx in Docker + # connects from 172.x.x.x, not 127.0.0.1). Without this, Gradio + # generates http:// file URLs even when accessed over HTTPS, causing + # mixed-content errors that block audio playback in the browser. + - FORWARDED_ALLOW_IPS=* restart: unless-stopped nginx: From 4af38a9ddcd67a8b55bc852fd137fc354d750d40 Mon Sep 17 00:00:00 2001 From: Daniel Cox Date: Wed, 12 Aug 2026 10:05:53 +0930 Subject: [PATCH 4/8] fix: replace FORWARDED_ALLOW_IPS with nginx map, add .dockerignore HTTPS proxy handling moved entirely to nginx (map $http_x_forwarded_proto). - .dockerignore to exclude models/data/lora/output from build context - nginx: manifest.json returns proper JSON for PWA, favicon returns 204 - docker-compose: drop version key, remove FORWARDED_ALLOW_IPS env var - README: update HTTPS troubleshooting (nginx map handles it now) --- .dockerignore | 24 ++++++++++++++++++++++++ docker/README.md | 4 ++-- docker/docker-compose.yml | 8 +------- docker/nginx.conf | 23 ++++++++++++++++++++++- lora_ft_webui.py | 2 -- 5 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b9f323e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,24 @@ +# Model weights (mounted at runtime via volumes) +models/ +data/ +lora/ +output/ + +# Git history +.git/ + +# Python cache +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +.venv/ + +# Docker config (not needed inside image) +docker/docker-compose.yml +docker/nginx.conf +docker/README.md + +# IDE / OS +.DS_Store +.vscode/ diff --git a/docker/README.md b/docker/README.md index 40570b3..b7bbd0e 100644 --- a/docker/README.md +++ b/docker/README.md @@ -121,7 +121,7 @@ docker run --gpus all -p 7860:7860 \ |----------|---------|-------------| | `GRADIO_SERVER_PORT` | `7860` | Port for the WebUI server | | `GRADIO_ROOT_PATH` | `""` | URL prefix when behind a reverse proxy (e.g., `/webui`) | -| `FORWARDED_ALLOW_IPS` | `*` | IPs allowed to set `X-Forwarded-Proto`. Set to `*` when behind a Docker reverse proxy so Gradio generates `https://` file URLs. | +| `VOXCPM_LANG` | `zh` | Default UI language (`zh` or `en`) | ## Reverse Proxy @@ -154,4 +154,4 @@ docker compose -f docker/docker-compose.yml logs -f training-webui - **WebUI not accessible**: Check that port 80 (nginx) or 7860 (direct) isn't blocked by a firewall. - **WebSocket errors behind proxy**: Ensure your proxy forwards `Upgrade` and `Connection` headers (the included nginx.conf handles this). - **Health check failing**: Ensure nginx is running — `curl http://localhost/` should return `OK`. -- **Mixed-content / audio not playing over HTTPS**: Gradio generates `http://` file URLs because uvicorn doesn't trust the `X-Forwarded-Proto` header from the Docker bridge network. The compose file sets `FORWARDED_ALLOW_IPS=*` to fix this. If you run without compose, pass `-e FORWARDED_ALLOW_IPS=*` to `docker run`. +- **Mixed-content / audio not playing over HTTPS**: The nginx config uses `map $http_x_forwarded_proto` to pass the correct protocol through to Gradio. This ensures `https://` file URLs are generated when accessed via HTTPS through a load balancer. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8fea42d..5eb013a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: training-webui: build: @@ -32,11 +30,7 @@ services: environment: - GRADIO_SERVER_PORT=7860 - GRADIO_ROOT_PATH=/webui # Matches nginx location block - # Tell uvicorn to trust X-Forwarded-Proto from any IP (nginx in Docker - # connects from 172.x.x.x, not 127.0.0.1). Without this, Gradio - # generates http:// file URLs even when accessed over HTTPS, causing - # mixed-content errors that block audio playback in the browser. - - FORWARDED_ALLOW_IPS=* + # - VOXCPM_LANG=en # Uncomment for English UI (default: zh) restart: unless-stopped nginx: diff --git a/docker/nginx.conf b/docker/nginx.conf index 5e5ea01..33fedd1 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,19 +1,40 @@ +# Preserve X-Forwarded-Proto from upstream load balancer (e.g. AWS ALB). +# If ALB already set it to "https", pass that through instead of $scheme +# (which is "http" since ALB→nginx is unencrypted). Falls back to $scheme +# when accessed directly (no upstream proxy). +map $http_x_forwarded_proto $forwarded_proto { + default $http_x_forwarded_proto; + "" $scheme; +} + server { listen 80; server_name _; + absolute_redirect off; + # Health check for load balancers (AWS ALB, etc.) location = / { return 200 'OK\n'; add_header Content-Type text/plain; } + location = /manifest.json { + return 200 '{"name":"VoxCPM Training","short_name":"VoxCPM","start_url":"/webui/"}'; + default_type application/json; + } + + location = /favicon.ico { + return 204; + access_log off; + } + location /webui/ { proxy_pass http://training-webui:7860/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $forwarded_proto; # WebSocket support (required for Gradio) proxy_http_version 1.1; diff --git a/lora_ft_webui.py b/lora_ft_webui.py index 47a4079..69f272a 100644 --- a/lora_ft_webui.py +++ b/lora_ft_webui.py @@ -1323,9 +1323,7 @@ with gr.Blocks(title="VoxCPM LoRA WebUI", theme=gr.themes.Soft(), css=custom_css ) if __name__ == "__main__": - # Ensure lora directory exists os.makedirs("lora", exist_ok=True) - port = int(os.environ.get("GRADIO_SERVER_PORT", "7860")) root_path = os.environ.get("GRADIO_ROOT_PATH", "") From e0369f0c7b7dd98305009f009174e58da4098406 Mon Sep 17 00:00:00 2001 From: Daniel Cox Date: Wed, 2 Sep 2026 12:28:04 +0930 Subject: [PATCH 5/8] fix: nginx health check probes backend instead of static 200 --- docker/README.md | 5 ++--- docker/nginx.conf | 7 +++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docker/README.md b/docker/README.md index b7bbd0e..556c693 100644 --- a/docker/README.md +++ b/docker/README.md @@ -83,11 +83,10 @@ The Dockerfile creates empty `/app/models`, `/app/lora`, `/app/output` directori ## Health Check -The nginx proxy responds with `200 OK` on `GET /` for load balancer health checks (AWS ALB, etc.). This is separate from the WebUI at `/webui/`. +The nginx proxy forwards `GET /` to the training-webui backend, so load balancer health checks (AWS ALB, etc.) reflect real application health — returning 502 when the backend is down. This is separate from the WebUI at `/webui/`. ```bash curl http://localhost/ -# OK ``` ## Direct Access (no proxy) @@ -153,5 +152,5 @@ docker compose -f docker/docker-compose.yml logs -f training-webui - **OOM errors**: Reduce batch size in the WebUI or use a GPU with more VRAM. - **WebUI not accessible**: Check that port 80 (nginx) or 7860 (direct) isn't blocked by a firewall. - **WebSocket errors behind proxy**: Ensure your proxy forwards `Upgrade` and `Connection` headers (the included nginx.conf handles this). -- **Health check failing**: Ensure nginx is running — `curl http://localhost/` should return `OK`. +- **Health check failing**: Ensure the training-webui container is running — `curl http://localhost/` proxies to the backend and returns 502 if it's unreachable. - **Mixed-content / audio not playing over HTTPS**: The nginx config uses `map $http_x_forwarded_proto` to pass the correct protocol through to Gradio. This ensures `https://` file URLs are generated when accessed via HTTPS through a load balancer. diff --git a/docker/nginx.conf b/docker/nginx.conf index 33fedd1..5af3f9c 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -15,8 +15,11 @@ server { # Health check for load balancers (AWS ALB, etc.) location = / { - return 200 'OK\n'; - add_header Content-Type text/plain; + proxy_pass http://training-webui:7860/; + proxy_set_header Host $host; + proxy_read_timeout 5s; + proxy_connect_timeout 3s; + access_log off; } location = /manifest.json { From 6912e0ffa58515427ce5e2da56ce3f69045eed3b Mon Sep 17 00:00:00 2001 From: Daniel Cox Date: Wed, 2 Sep 2026 12:28:20 +0930 Subject: [PATCH 6/8] fix: remove reference to non-existent deploy/Dockerfile.voxcpm-unified --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 556c693..e5b2abd 100644 --- a/docker/README.md +++ b/docker/README.md @@ -71,7 +71,7 @@ Use absolute container paths (`/app/data/...`) in your manifest so the container If `models/openbmb__VoxCPM2/` exists on the host, the app loads directly from that path — no network access needed. If the directory is empty or missing, `from_pretrained` falls back to `snapshot_download` from HuggingFace Hub. -The Dockerfile sets `HF_HOME=/app/models` so any Hub downloads land in the same mounted volume (matching the pattern in `deploy/Dockerfile.voxcpm-unified`). This means models persist across container restarts regardless of whether they were pre-populated or auto-downloaded. +The Dockerfile sets `HF_HOME=/app/models` so any Hub downloads land in the same mounted volume. This means models persist across container restarts regardless of whether they were pre-populated or auto-downloaded. **Recommended:** Pre-populate to avoid first-run download delay: From 99a205d483aad390f11b13e88aa04c48729127c6 Mon Sep 17 00:00:00 2001 From: Daniel Cox Date: Wed, 2 Sep 2026 12:28:37 +0930 Subject: [PATCH 7/8] fix: remove VOXCPM_LANG env var (depends on i18n PR #380) --- docker/README.md | 1 - docker/docker-compose.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/docker/README.md b/docker/README.md index e5b2abd..5d0f261 100644 --- a/docker/README.md +++ b/docker/README.md @@ -120,7 +120,6 @@ docker run --gpus all -p 7860:7860 \ |----------|---------|-------------| | `GRADIO_SERVER_PORT` | `7860` | Port for the WebUI server | | `GRADIO_ROOT_PATH` | `""` | URL prefix when behind a reverse proxy (e.g., `/webui`) | -| `VOXCPM_LANG` | `zh` | Default UI language (`zh` or `en`) | ## Reverse Proxy diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5eb013a..5e99f04 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -30,7 +30,6 @@ services: environment: - GRADIO_SERVER_PORT=7860 - GRADIO_ROOT_PATH=/webui # Matches nginx location block - # - VOXCPM_LANG=en # Uncomment for English UI (default: zh) restart: unless-stopped nginx: From b74c73f3e03d67b2604e833d42d55299e6539b78 Mon Sep 17 00:00:00 2001 From: Daniel Cox Date: Wed, 2 Sep 2026 12:28:48 +0930 Subject: [PATCH 8/8] fix: .dockerignore exclude venv/ and .venv-bench/ --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index b9f323e..ea5c99b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,6 +13,8 @@ __pycache__/ *.pyo *.egg-info/ .venv/ +venv/ +.venv-bench/ # Docker config (not needed inside image) docker/docker-compose.yml