Skip to content

feat: add Docker + web deployment support#161

Merged
jamiepine merged 3 commits intojamiepine:mainfrom
ageofalgo:feat/docker-web-deployment
Mar 13, 2026
Merged

feat: add Docker + web deployment support#161
jamiepine merged 3 commits intojamiepine:mainfrom
ageofalgo:feat/docker-web-deployment

Conversation

@ageofalgo
Copy link

@ageofalgo ageofalgo commented Feb 23, 2026

feat: Docker + Web Deployment Support

Summary

Adds production-ready Docker support for running Voicebox as a self-contained web service (API + UI) without the Tauri desktop app. This implements the Docker deployment planned in DOCKER_DEPLOYMENT.md.

Motivation

Currently, Voicebox can only be used via the Tauri desktop app or by running the backend manually. This PR enables a simple docker compose up workflow that serves both the API and the web frontend from a single container — useful for headless servers, integration with other local tools, and anyone who doesn't want to install Python/Bun/Rust locally.

Changes

New Files

File Description
Dockerfile 3-stage build: (1) Bun builds the web/ frontend, (2) Python deps compiled in builder, (3) slim runtime image with both frontend + backend
docker-compose.yml Localhost-only port binding (17493), bind-mount for audio output, named volumes for data persistence and HuggingFace model cache
.dockerignore Excludes desktop-only dirs (tauri/, landing/, docs/) and build artifacts for a lean image

Modified Files

File Change
backend/main.py Root GET / endpoint now serves index.html when the frontend build is present (Docker), falls back to API JSON otherwise. Added conditional static file serving + SPA catch-all route, only activated when /app/frontend/ exists — zero impact on existing non-Docker usage.

Usage

# Build image
docker compose build

# Start container
docker compose up -d

# Open in browser
open http://localhost:17493

# Check health
curl http://localhost:17493/health

# View logs
docker compose logs -f

# Stop
docker compose down

Generated audio files appear in the ./output/ directory (configurable via the bind-mount in docker-compose.yml).

Architecture

┌──────────────────────────────────────────┐
│            Docker Container              │
│                                          │
│  ┌──────────────────────────────────┐    │
│  │  FastAPI (uvicorn :17493)        │    │
│  │                                  │    │
│  │  GET /         → index.html      │    │
│  │  GET /assets/* → static files    │    │
│  │  GET /health   → health JSON     │    │
│  │  POST /generate → TTS engine     │    │
│  │  GET /*        → SPA catch-all   │    │
│  └──────────────────────────────────┘    │
│                                          │
│  /app/frontend/  ← built web/ React app  │
│  /app/backend/   ← Python FastAPI code   │
│  /app/data/      ← DB, profiles, cache   │
└──────────────────────────────────────────┘
         │
         ├── 127.0.0.1:17493 (localhost only)
         └── ./output/ (bind-mount for audio)

Build Details

The Dockerfile uses a 3-stage build to keep the runtime image small:

  1. frontend stage (oven/bun:1): Strips tauri/landing workspaces from package.json, installs deps, runs vite build (skipping tsc due to pre-existing upstream type errors)
  2. backend-builder stage (python:3.11-slim): Installs Python deps including Qwen3-TTS into a prefix
  3. Runtime stage (python:3.11-slim): Copies built frontend + Python packages, runs as non-root voicebox user with health check

Security

  • Port bound to 127.0.0.1 only (not accessible from network)
  • Runs as non-root user inside container
  • Resource limits (4 CPUs, 8GB RAM) via deploy config
  • No changes to existing authentication or CORS behavior

Notes

  • The web/ build skips TypeScript checking (tsc) because there are pre-existing type errors in the shared app/ source. Vite handles the build fine without strict type checking. These errors should be fixed upstream separately.
  • The frontend static file serving is conditionally activated — it only runs when /app/frontend/ exists (i.e., inside the Docker container). Running the backend outside Docker behaves exactly as before.
  • Models are auto-downloaded from HuggingFace on first generation. The huggingface-cache named volume ensures they persist across container rebuilds.

Testing

  • docker compose build succeeds
  • docker compose up -d starts container
  • curl http://localhost:17493/health returns healthy status
  • Web UI loads at http://localhost:17493
  • API endpoints remain functional
  • Running backend outside Docker is unaffected (no /app/frontend/ → JSON response at /)

Summary by CodeRabbit

  • Chores
    • Added comprehensive Docker support with optimized multi-stage builds for frontend and backend
    • Included Docker Compose configuration for quick local deployment with restart policies and resource limits
    • Configured .dockerignore to streamline build context and reduce image size
    • Runtime hardening: non-root runtime and health checks for improved stability

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aa0f4e8c-456f-4449-ac7e-095b592c1469

📥 Commits

Reviewing files that changed from the base of the PR and between 5a3f3ba and 50e01d1.

📒 Files selected for processing (1)
  • docker-compose.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • docker-compose.yml

📝 Walkthrough

Walkthrough

Adds Docker containerization: a comprehensive .dockerignore, a multi-stage Dockerfile (frontend build, Python deps, runtime image), and a docker-compose.yml to define the voicebox service, volumes, network, port binding, and resource limits.

Changes

Cohort / File(s) Summary
Docker containerization
/.dockerignore, ./Dockerfile, ./docker-compose.yml
Adds complete Docker setup: .dockerignore excludes non-essential files from build context; Dockerfile implements a three-stage build (frontend via bun/Vite, Python dependency build, production runtime with non-root user, healthcheck, exposed port 17493); docker-compose.yml defines the voicebox service, local port binding (127.0.0.1:17493), mounts (./output/app/data/generations), named volumes (voicebox-data, huggingface-cache), network voicebox-net, environment variable LOG_LEVEL=info, and resource limits (4 CPUs, 8G memory).

Sequence Diagram(s)

sequenceDiagram
    participant Dev as Developer
    participant Build as Docker Build
    participant Front as Frontend Stage
    participant Back as Backend-builder Stage
    participant Runtime as Runtime Image
    participant Compose as docker-compose
    participant Client as Client

    Dev->>Build: docker build (context)
    Build->>Front: run bun/Vite build (frontend assets)
    Build->>Back: install Python deps into /install
    Front-->>Runtime: copy built frontend assets
    Back-->>Runtime: copy preinstalled Python packages and backend code
    Build-->>Runtime: finalize image (non-root, healthcheck, expose 17493)
    Dev->>Compose: docker-compose up (uses built image)
    Compose->>Runtime: start container with volumes, env, networks, limits
    Client->>Runtime: HTTP requests to 127.0.0.1:17493
    Runtime-->>Client: API responses
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through files with glee,

Built stages, caches, ports for me.
Frontend, backend—stacked so neat,
Volumes, limits, healthchecks meet.
A carrot-deployed container treat! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add Docker + web deployment support' accurately summarizes the main changes: adding Dockerfile, docker-compose.yml, and .dockerignore for Docker and web deployment functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docker-compose.yml`:
- Around line 23-25: TTS_MODE is defined in docker-compose but not read by the
app; either remove TTS_MODE from docker-compose/docs or add code to consume it
by reading os.getenv("TTS_MODE") (or via the existing config loader / Settings
class) and wiring it into the TTS provider selection code (e.g., where the app
chooses between local vs remote TTS in the TTSManager/initialize_tts function)
so the environment variable controls provider choice; update tests/docs
accordingly.
- Around line 11-16: The bind-mount ./output:/app/data/generations can become
root-owned on the host and block the container user voicebox from writing; fix
by either (A) switching to a named volume instead of the host bind-mount so
Docker manages ownership, or (B) ensuring the host directory ./output is
pre-created and chowned to the same UID/GID as the voicebox user before starting
the container (or add an init step/entrypoint that creates /app/data/generations
and chowns it to voicebox at runtime). Reference the mount ./output, container
path /app/data/generations, and the voicebox user when implementing the change
or documenting the prerequisite.

In `@Dockerfile`:
- Around line 36-37: Pin the GitHub dependencies to immutable commit SHAs to
make builds reproducible: replace the moving refs by appending a specific commit
SHA to each URL—update the Dockerfile RUN line installing
git+https://github.com/QwenLM/Qwen3-TTS.git to use
git+https://github.com/QwenLM/Qwen3-TTS.git@<commit-sha>, and update the backend
requirements entries for the packages declared as linacodec @
git+https://github.com/ysharma3501/LinaCodec.git and Zipvoice @
git+https://github.com/ysharma3501/LuxTTS.git to linacodec @
git+https://github.com/ysharma3501/LinaCodec.git@<commit-sha> and Zipvoice @
git+https://github.com/ysharma3501/LuxTTS.git@<commit-sha> respectively (choose
the appropriate commit SHAs and test that pip installs still succeed).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 179b3e52-0f1b-491b-b90c-360e645c393e

📥 Commits

Reviewing files that changed from the base of the PR and between 3e6513c and 5a3f3ba.

📒 Files selected for processing (3)
  • .dockerignore
  • Dockerfile
  • docker-compose.yml

Comment on lines +11 to +16
volumes:
# Bind-mount for generated audio (customize the host path as needed)
# Host side: ./output/
# Container side: /app/data/generations/
- ./output:/app/data/generations

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify mount + host directory writeability prerequisites.
set -e

echo "Checking compose mount definition..."
rg -n '^\s*-\s*\./output:/app/data/generations' docker-compose.yml

echo "Checking explicit user override in compose..."
rg -n '^\s*user:\s*' docker-compose.yml || true

echo "Checking host output directory permissions (if present)..."
if [ -d output ]; then
  stat -c '%u:%g %a %n' output
  [ -w output ] && echo "output is writable by current host user" || echo "output is NOT writable by current host user"
else
  echo "output directory is missing (Docker may create it with root ownership depending on host setup)."
fi

Repository: jamiepine/voicebox

Length of output: 341


🏁 Script executed:

git ls-files | grep -i dockerfile

Repository: jamiepine/voicebox

Length of output: 72


🏁 Script executed:

cat -n Dockerfile

Repository: jamiepine/voicebox

Length of output: 2957


🏁 Script executed:

cat -n docker-compose.yml

Repository: jamiepine/voicebox

Length of output: 1277


Fix bind-mount write permissions for generated audio output.

The container runs as non-root user voicebox (Dockerfile lines 44–69), and /app/data/generations is created with voicebox ownership (line 65–66). However, the bind-mount ./output:/app/data/generations (line 15) overlays the host directory, which Docker will create as root-owned if it doesn't exist. This will cause write failures when the voicebox user attempts to save generated audio.

Ensure ./output is pre-created with permissions that allow the container user to write, or document this prerequisite for users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose.yml` around lines 11 - 16, The bind-mount
./output:/app/data/generations can become root-owned on the host and block the
container user voicebox from writing; fix by either (A) switching to a named
volume instead of the host bind-mount so Docker manages ownership, or (B)
ensuring the host directory ./output is pre-created and chowned to the same
UID/GID as the voicebox user before starting the container (or add an init
step/entrypoint that creates /app/data/generations and chowns it to voicebox at
runtime). Reference the mount ./output, container path /app/data/generations,
and the voicebox user when implementing the change or documenting the
prerequisite.

Comment on lines +36 to +37
RUN pip install --no-cache-dir --prefix=/install \
git+https://github.com/QwenLM/Qwen3-TTS.git
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether GitHub git+ dependencies are pinned to immutable refs.
python - <<'PY'
import pathlib, re
paths = ["Dockerfile", "backend/requirements.txt"]
pat = re.compile(r'git\+https://github\.com/\S+')
for p in paths:
    txt = pathlib.Path(p).read_text()
    for i, line in enumerate(txt.splitlines(), 1):
        if "git+https://github.com/" in line:
            pinned = "@" in line.split("git+https://github.com/",1)[1]
            status = "PINNED" if pinned else "UNPINNED"
            print(f"{p}:{i}: {status}: {line.strip()}")
PY

Repository: jamiepine/voicebox

Length of output: 326


Pin GitHub dependencies to immutable refs across the repository.

Line 37 in the Dockerfile and lines 21-22 in backend/requirements.txt install from moving branches/HEAD, which makes builds non-reproducible and weakens supply-chain guarantees.

All three instances require pinning to a commit SHA:

  • Dockerfile:37: git+https://github.com/QwenLM/Qwen3-TTS.git
  • backend/requirements.txt:21: linacodec @ git+https://github.com/ysharma3501/LinaCodec.git
  • backend/requirements.txt:22: Zipvoice @ git+https://github.com/ysharma3501/LuxTTS.git
🔧 Proposed fix example (Dockerfile)
+ARG QWEN3_TTS_REF=<<commit-sha>>
 RUN pip install --no-cache-dir --prefix=/install \
-    git+https://github.com/QwenLM/Qwen3-TTS.git
+    git+https://github.com/QwenLM/Qwen3-TTS.git@${QWEN3_TTS_REF}

For requirements.txt, append @<commit-sha> to each URL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 36 - 37, Pin the GitHub dependencies to immutable
commit SHAs to make builds reproducible: replace the moving refs by appending a
specific commit SHA to each URL—update the Dockerfile RUN line installing
git+https://github.com/QwenLM/Qwen3-TTS.git to use
git+https://github.com/QwenLM/Qwen3-TTS.git@<commit-sha>, and update the backend
requirements entries for the packages declared as linacodec @
git+https://github.com/ysharma3501/LinaCodec.git and Zipvoice @
git+https://github.com/ysharma3501/LuxTTS.git to linacodec @
git+https://github.com/ysharma3501/LinaCodec.git@<commit-sha> and Zipvoice @
git+https://github.com/ysharma3501/LuxTTS.git@<commit-sha> respectively (choose
the appropriate commit SHAs and test that pip installs still succeed).

TTS_MODE is not read by any code in the backend — it only exists in
unimplemented planning docs. Remove it to avoid confusing users.
@jamiepine jamiepine merged commit 986a748 into jamiepine:main Mar 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants