Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
46 changes: 46 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Version control
.git
.github
.gitignore

# Desktop-only (not needed in web container)
tauri/
landing/
docs/
mlx-test/
scripts/

# Dependencies & build artifacts (rebuilt in Docker)
node_modules/
__pycache__/
*.pyc
*.pyo
*.egg-info/
dist/
build/
*.spec

# Data (will be bind-mounted)
data/
backend/data/

# IDE & OS
.vscode/
.idea/
*.swp
*.swo
.DS_Store
Thumbs.db

# Config files not needed in container
biome.json
.biomeignore
.bumpversion.cfg
.npmrc
Makefile
CHANGELOG.md
CONTRIBUTING.md
SECURITY.md
LICENSE
README.md
backend/README.md
79 changes: 79 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# ============================================================
# Voicebox — Local TTS Server with Web UI (CPU)
# 3-stage build: Frontend → Python deps → Runtime
# ============================================================

# === Stage 1: Build frontend ===
FROM oven/bun:1 AS frontend

WORKDIR /build

# Copy workspace config and frontend source
COPY package.json bun.lock ./
COPY app/ ./app/
COPY web/ ./web/

# Strip workspaces not needed for web build, and fix trailing comma
RUN sed -i '/"tauri"/d; /"landing"/d' package.json && \
sed -i -z 's/,\n ]/\n ]/' package.json
RUN bun install --no-save
# Build frontend (skip tsc — upstream has pre-existing type errors)
RUN cd web && bunx --bun vite build


# === Stage 2: Build Python dependencies ===
FROM python:3.11-slim AS backend-builder

WORKDIR /build

RUN apt-get update && apt-get install -y --no-install-recommends \
git \
build-essential \
&& rm -rf /var/lib/apt/lists/*

COPY backend/requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
RUN pip install --no-cache-dir --prefix=/install \
git+https://github.com/QwenLM/Qwen3-TTS.git
Comment on lines +36 to +37
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).



# === Stage 3: Runtime ===
FROM python:3.11-slim

# Create non-root user for security
RUN groupadd -r voicebox && \
useradd -r -g voicebox -m -s /bin/bash voicebox

WORKDIR /app

# Install only runtime system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
curl \
&& rm -rf /var/lib/apt/lists/*

# Copy installed Python packages from builder stage
COPY --from=backend-builder /install /usr/local

# Copy backend application code
COPY --chown=voicebox:voicebox backend/ /app/backend/

# Copy built frontend from frontend stage
COPY --from=frontend --chown=voicebox:voicebox /build/web/dist /app/frontend/

# Create data directories owned by non-root user
RUN mkdir -p /app/data/generations /app/data/profiles /app/data/cache \
&& chown -R voicebox:voicebox /app/data

# Switch to non-root user
USER voicebox

# Expose the API port
EXPOSE 17493

# Health check — auto-restart if the server hangs
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=60s \
CMD curl -f http://localhost:17493/health || exit 1

# Start the FastAPI server
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "17493"]
42 changes: 42 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
services:
voicebox:
build: .
container_name: voicebox
restart: unless-stopped

ports:
# Bind to localhost only for security
- "127.0.0.1:17493:17493"

volumes:
# Bind-mount for generated audio (customize the host path as needed)
# Host side: ./output/
# Container side: /app/data/generations/
- ./output:/app/data/generations

Comment on lines +11 to +16
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.

# Named volume for profiles, DB, cache (persists across container restarts)
- voicebox-data:/app/data

# HuggingFace model cache (so models aren't re-downloaded on rebuild)
- huggingface-cache:/home/voicebox/.cache/huggingface

environment:
- TTS_MODE=local
- LOG_LEVEL=info

networks:
- voicebox-net

deploy:
resources:
limits:
cpus: '4'
memory: 8G

networks:
voicebox-net:
driver: bridge

volumes:
voicebox-data:
huggingface-cache: