AIDockerLLM

Speaches Whisper API with Docker: Your Own Speech-to-Text Server (Linux, Windows, Mac)

Before you start

  • 13 min read

Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#

Speaches is an open-source server that gives you the same audio API as OpenAI, but runs Whisper models on your own computer, so your recordings never leave it.

Under the hood it uses faster-whisper for speech-to-text, and Kokoro and Piper for text-to-speech. Any tool that can talk to OpenAI's /v1/audio/transcriptions can talk to Speaches instead: you only change the base URL. Models load when the first request needs them and unload after a while without use, so a small box stays calm. People use it to transcribe meetings, add voice input to Open WebUI, and cut the per-minute bill of a cloud service.

WhatValue
Version used in this guidev0.8.3 (last stable release, Sep 2025). The newest release on GitHub is v0.9.0-rc.3 (a release candidate, Dec 2025)
Image tagsghcr.io/speaches-ai/speaches:0.8.3-cpu and ghcr.io/speaches-ai/speaches:0.8.3-cuda-12.6.3 (both checked in the registry on 2026-10-26)
Time neededAbout 10 minutes, plus the model download
Ports8000 (API, docs at /docs, web UI)
Official docshttps://speaches.ai/

Why 0.8.3 and not the newest tag? The newest release is a release candidate with new diarization code, and one open issue (#679) reports unsafe model loading in it. The install problems people report (#671, #678) hit source installs, not the prebuilt image. We found no reports of data loss. So we pin the last stable release. If you need speaker diarization, try 0.9.0-rc.3-cpu and accept that it is a candidate.

One honest note: the repository looks quiet. The last commit we saw is from April 2026, and two issues (#659, #622) ask whether it is still maintained. It works today. If it stalls, plain faster-whisper is the fallback.

How it fits together#

Your app sends audio to the Speaches API on port 8000, Speaches loads a faster-whisper model from its cache, and the text comes back.

How Speaches works: Open WebUI or your own app sends mic clips and uploads an audio file to the OpenAI-style Speaches API on port 8000, which hands the audio to a faster-whisper model that loads on demand from a model cache folder (filled once from Hugging Face), and the text comes back as json, srt or vtt

The model cache lives in a folder on your disk, so a restart does not download the models again.

Before you start#

You need Docker, about 4 GB of free disk and a few GB of RAM. A GPU is optional.

SystemRequirementsTest status
LinuxDocker Engine with the Compose plugin, bash, curl. For a GPU: NVIDIA driver plus NVIDIA Container ToolkitChecked against the official docs
WindowsDocker Desktop with the WSL2 backend, PowerShell 7. For a GPU: NVIDIA driver with WSL2 GPU support (VERIFY on your machine)Checked against the official docs
MacDocker Desktop, Terminal. No CUDA in containers, so use the CPU imageChecked against the official docs

Check your tools:

Run in terminalbash
docker --version
docker compose version

Checklist:

  • [ ] Docker is running
  • [ ] Port 8000 is free on your machine
  • [ ] You have a backup of any older ~/docker_volumes/speaches folder

Tested on 2026-10-26: Linux = not run, Windows = not run, Mac = not run. The commands were checked against the official Speaches docs, the v0.8.3 source and the image registry (see Sources).

Step 1: Create the folder, the key and the Compose file#

Make a folder for the model cache, create a secret API key, and write a small compose.yaml that pins the image.

Models are big, so we keep the cache in ~/docker_volumes/speaches/hf-cache. The container runs as user ubuntu with uid 1000, so on Linux the folder must belong to uid 1000 or you get a permission error.

Linux#

Run in terminalbash
mkdir -p ~/docker_volumes/speaches/hf-cache && cd ~/docker_volumes/speaches
sudo chown -R 1000:1000 hf-cache
echo "SPEACHES_API_KEY=$(openssl rand -hex 24)" > .env && chmod 600 .env
cat > compose.yaml <<'EOF'
services:
  speaches:
    image: ghcr.io/speaches-ai/speaches:0.8.3-cpu
    container_name: speaches
    restart: unless-stopped
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      - API_KEY=${SPEACHES_API_KEY}
      - WHISPER__TTL=300
      - LOG_LEVEL=info
    volumes:
      - ./hf-cache:/home/ubuntu/.cache/huggingface/hub
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s
EOF

Have an NVIDIA GPU? Change the image to ghcr.io/speaches-ai/speaches:0.8.3-cuda-12.6.3 and add this block under the service:

File contentyaml
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

Windows (PowerShell)#

Codepowershell
$d = "$HOME\docker_volumes\speaches"
New-Item -ItemType Directory -Force "$d\hf-cache" | Out-Null
Set-Location $d
$bytes = New-Object byte[] 24
[System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
"SPEACHES_API_KEY=" + (($bytes | ForEach-Object { $_.ToString('x2') }) -join '') | Out-File .env -Encoding ascii
@'
services:
  speaches:
    image: ghcr.io/speaches-ai/speaches:0.8.3-cpu
    container_name: speaches
    restart: unless-stopped
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      - API_KEY=${SPEACHES_API_KEY}
      - WHISPER__TTL=300
      - LOG_LEVEL=info
    volumes:
      - ./hf-cache:/home/ubuntu/.cache/huggingface/hub
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s
'@ | Set-Content compose.yaml -Encoding utf8

For a GPU, use the 0.8.3-cuda-12.6.3 image and the deploy: block from the Linux step. Windows bind mounts on NTFS can be slow. For heavy use, create the folder inside WSL2 instead (VERIFY on your setup).

Mac (Terminal)#

Same commands as Linux, but skip the sudo chown line (Docker Desktop handles the permissions). Keep the CPU image.

Run in terminalbash
mkdir -p ~/docker_volumes/speaches/hf-cache && cd ~/docker_volumes/speaches
echo "SPEACHES_API_KEY=$(openssl rand -hex 24)" > .env && chmod 600 .env
# then create compose.yaml exactly as in the Linux step (CPU image)

Check it worked: ls shows .env, compose.yaml and hf-cache. Never commit .env.

Step 2: Start Speaches#

Start the container and wait for the health check.

Linux#

Run in terminalbash
docker compose up -d
docker compose ps
curl -s http://localhost:8000/health

Windows (PowerShell)#

Run in terminalpowershell
docker compose up -d
docker compose ps
curl.exe -s http://localhost:8000/health

Mac (Terminal)#

Same commands as Linux:

Run in terminalbash
docker compose up -d
docker compose ps
curl -s http://localhost:8000/health

Check it worked: docker compose ps shows speaches as running (and healthy after a short wait), and /health answers with an OK status. The first start pulls the image, so give it a minute. Interactive API docs are at http://localhost:8000/docs.

Step 3: Download a speech model#

Speaches downloads a model when you ask for it. Ask once, then it stays in your cache folder.

Systran/faster-whisper-small is a good first choice: small, multilingual and fast on a CPU. For better accuracy on a GPU, take Systran/faster-distil-whisper-large-v3 (the model the Open WebUI docs use). In version 0.8.3 there is no "download on startup" setting, so you download with one request.

Linux#

Run in terminalbash
set -a; source .env; set +a
curl -X POST -H "Authorization: Bearer $SPEACHES_API_KEY" \
  http://localhost:8000/v1/models/Systran/faster-whisper-small
curl -H "Authorization: Bearer $SPEACHES_API_KEY" http://localhost:8000/v1/models

Windows (PowerShell)#

Codepowershell
$key = ((Get-Content .env) -replace '^SPEACHES_API_KEY=','').Trim()
$h = @{ Authorization = "Bearer $key" }
Invoke-RestMethod -Method Post -Headers $h -Uri http://localhost:8000/v1/models/Systran/faster-whisper-small
Invoke-RestMethod -Headers $h -Uri http://localhost:8000/v1/models | ConvertTo-Json -Depth 5

Mac (Terminal)#

Same commands as Linux:

Run in terminalbash
set -a; source .env; set +a
curl -X POST -H "Authorization: Bearer $SPEACHES_API_KEY" \
  http://localhost:8000/v1/models/Systran/faster-whisper-small
curl -H "Authorization: Bearer $SPEACHES_API_KEY" http://localhost:8000/v1/models

Check it worked: the model list now contains Systran/faster-whisper-small. To see everything you can download, ask /v1/registry?task=automatic-speech-recognition. Potential issue: a 422 error on this download call was reported (issue #655). If you hit it, use the speaches-cli tool from the docs: uvx speaches-cli model download Systran/faster-whisper-small.

Step 4: Transcribe your first file#

Send an audio file as a form upload, exactly like you would to OpenAI.

Use any short recording named audio.mp3 (wav, m4a and others also work).

Linux#

Run in terminalbash
curl http://localhost:8000/v1/audio/transcriptions \
  -H "Authorization: Bearer $SPEACHES_API_KEY" \
  -F [email protected] \
  -F model=Systran/faster-whisper-small \
  -F response_format=json

Windows (PowerShell)#

PowerShell 7 can send the form itself:

Codepowershell
Invoke-RestMethod -Method Post -Headers $h -Uri http://localhost:8000/v1/audio/transcriptions `
  -Form @{ file = Get-Item .\audio.mp3; model = "Systran/faster-whisper-small" }

Windows PowerShell 5.1 has no -Form. Use curl.exe with the same -F options as the Linux command.

Mac (Terminal)#

Same commands as Linux.

Check it worked: you get a JSON answer with a text field (expected shape, from the OpenAI reference: {"text": "..."}).

Useful options, all from the OpenAI audio API:

  • language=ja (an ISO-639-1 code) is faster and more accurate than auto-detect. Leave it out to detect the language.
  • response_format=verbose_json adds segments and timestamps. srt, vtt and text work too.
  • stream=true sends the text back piece by piece (server-sent events) while it is still being transcribed.
  • Translation to English uses POST /v1/audio/translations with the same form fields.

Step 5: Connect Open WebUI#

Point Open WebUI's speech-to-text engine at Speaches and talk to your chat.

The setup is the same on every system, because it is a Compose file.

Linux#

Both containers must be on the same Docker network so Open WebUI can reach http://speaches:8000. A simple way is to run them from one Compose file. Add this service next to speaches in compose.yaml (pin a release tag instead of main for a stable setup):

File contentyaml
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:8080"
    environment:
      - AUDIO_STT_ENGINE=openai
      - AUDIO_STT_OPENAI_API_BASE_URL=http://speaches:8000/v1
      - AUDIO_STT_OPENAI_API_KEY=${SPEACHES_API_KEY}
      - AUDIO_STT_MODEL=Systran/faster-whisper-small
    volumes:
      - ./open-webui:/app/backend/data

Windows (PowerShell)#

Same Compose service, then run docker compose up -d in $HOME\docker_volumes\speaches.

Mac (Terminal)#

Same commands as Linux: add the service and run docker compose up -d.

You can also set it in the UI: Admin Settings, Audio tab, engine "OpenAI", base URL http://speaches:8000/v1, your key, and the model name. The Speaches docs warn that the environment variables do not override settings you already saved in the UI. Download the model first (Step 3), or the first voice message fails.

Text-to-speech, realtime and languages#

The same server can also speak, and it has a realtime API.

  • Text-to-speech. Download speaches-ai/Kokoro-82M-v1.0-ONNX, then POST /v1/audio/speech with a voice such as af_heart. The docs say extra steps are needed for TTS (see the install page), and opus and aac output are not supported. Chinese TTS currently fails (issue #647).
  • Realtime API. Added in v0.8.0, including WebRTC. See the Speaches realtime docs.
  • Languages. Whisper models are multilingual, including Japanese and Hindi. The distil models are English-focused, so pick faster-whisper-large-v3 or a smaller multilingual model for other languages (VERIFY the model id in /v1/registry).

Which model size?#

These are rough ballparks from general knowledge, not from the Speaches docs (VERIFY with the benchmark table in the faster-whisper README):

ModelRough VRAM (fp16)Good for
tiny, baseabout 1 GBQuick tests, weak accuracy
smallabout 2 GBGood CPU choice
mediumabout 5 GBBetter accuracy
large-v35 to 10 GBBest accuracy
distil-large-v3less than large-v3Faster, English-focused

Put it behind HTTPS with Traefik#

On a server with a domain, let Traefik handle HTTPS and remove Speaches' published port.

This assumes Traefik v3 already runs with the Docker provider, shares a Docker network called proxy, has an entrypoint called https and a certificate for your hostname. Local-only users can skip this section. Replace the ports: and add labels:

File contentyaml
services:
  speaches:
    image: ghcr.io/speaches-ai/speaches:0.8.3-cpu
    restart: unless-stopped
    env_file:
      - .env
    environment:
      - API_KEY=${SPEACHES_API_KEY}
    volumes:
      - ./hf-cache:/home/ubuntu/.cache/huggingface/hub
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.speaches.rule=Host(`stt.<your-domain>`)"
      - "traefik.http.routers.speaches.entrypoints=https"
      - "traefik.http.routers.speaches.tls=true"
      - "traefik.http.services.speaches.loadbalancer.server.port=8000"
networks:
  proxy:
    external: true

Create the network once if needed: docker network create proxy. If you use an ACME resolver, add traefik.http.routers.speaches.tls.certresolver=<your-resolver>.

Check it worked: curl -I https://stt.<your-domain>/health answers, and a transcription request without the key is refused.

Security and testing#

Speaches has no login by default, so the key and the network are your job.

ControlHow
API keyAPI_KEY in .env (chmod 600 .env). Clients send Authorization: Bearer <key>
NetworkPort bound to 127.0.0.1, or no host port at all behind Traefik
Web UISwitch it off with ENABLE_UI=false if you do not use it
ModelsDownload only models you trust, from the official registry
SecretsNever in the Compose file or in Traefik labels

Keep port 8000 off the internet. Do not forward it on your router. If you must reach it from outside, use Traefik with HTTPS and an IP allowlist, or a VPN.

Test it: docker compose ps shows a healthy container, docker logs speaches --tail 50 shows no errors, a request with a wrong key is rejected, and a request with the right key returns text.

Troubleshooting#

ProblemSolution
PermissionError: [Errno 13] Permission denied: '/home/ubuntu/.cache/huggingface/hub'The cache folder is not writable by uid 1000: sudo chown -R 1000:1000 ~/docker_volumes/speaches/hf-cache
could not select device driver "nvidia" with capabilities: [[gpu]]Install the NVIDIA Container Toolkit and restart Docker, or use the CPU image
401 or 403 from every requestThe key is wrong or missing. Send Authorization: Bearer <key>
Failed to decode audio. The provided file type is not supported. (415)Convert the file to wav or mp3 and try again
422 on POST /v1/models/<id>Known bug (#655). Use speaches-cli model download <id>
500 on a silent or empty fileKnown bug (#667, divide by zero). Do not send empty audio
Slow first requestThe model loads on the first call. Download it ahead (Step 3) and set WHISPER__TTL=-1 to keep it loaded
Open WebUI says it cannot transcribeCheck the base URL ends in /v1, the key matches, both containers share a network, and the model is downloaded

Source install errors (not the Docker image)#

If you run Speaches from source instead of Docker, you may see these. They are known issues and the Docker image avoids them:

  • ModuleNotFoundError: No module named 'gradio' (#671): run uv sync --extra ui.
  • ChatInterface.__init__() got an unexpected keyword argument 'type' (#678): gradio 6 is too new. Pin gradio<6.

Known bugs#

  • Unloading a model does not always give memory back (#638, #681).
  • Chinese TTS fails (#647).
  • Parakeet model downloads can be incomplete (#657).
  • CUDA does not work on ARM64 images (#620).

Diagnostics: docker logs speaches --tail 100, docker compose ps, docker inspect speaches.

Updates and backups#

Change the pinned tag, pull, restart. Back up your config; the models can be downloaded again.

Update: read the release notes at https://github.com/speaches-ai/speaches/releases, change the tag in compose.yaml, then:

Run in terminalbash
cd ~/docker_volumes/speaches && docker compose pull && docker compose up -d
Codepowershell
Set-Location "$HOME\docker_volumes\speaches"; docker compose pull; docker compose up -d

Backup script (Linux and Mac), saved as ~/docker_volumes/speaches/backup.sh. It skips the big model cache and keeps your config:

File contentbash
#!/usr/bin/env bash
set -e
cd ~/docker_volumes
mkdir -p speaches/backups
tar czf speaches/backups/speaches-$(date +%F).tgz --exclude=speaches/hf-cache --exclude=speaches/backups speaches

Schedule it at 03:00 with crontab -e: 0 3 * * * bash ~/docker_volumes/speaches/backup.sh. On Windows, use Task Scheduler with this PowerShell line:

Codepowershell
Compress-Archive -Path "$HOME\docker_volumes\speaches\.env","$HOME\docker_volumes\speaches\compose.yaml" -DestinationPath "$HOME\docker_volumes\speaches\backups\speaches-$(Get-Date -Format yyyy-MM-dd).zip" -Force

Create the backups folder first on Windows (New-Item -ItemType Directory -Force "$HOME\docker_volumes\speaches\backups").

Restore test (Linux and Mac): stop the original so port 8000 is free, then unpack into a scratch folder and start it.

Run in terminalbash
cd ~/docker_volumes/speaches && docker compose down
mkdir -p /tmp/restore && tar xzf backups/speaches-*.tgz -C /tmp/restore
cd /tmp/restore/speaches && mkdir -p hf-cache && docker compose up -d
curl -s http://localhost:8000/health

The restored server has no models yet. Download one again (Step 3) and transcribe a short file to prove it works. Then docker compose down there and start your real one again.

Performance and resources#

Use a GPU for anything bigger than small, and int8 on a CPU.

  • Watch it with docker stats.
  • WHISPER__COMPUTE_TYPE=int8 shrinks memory on a CPU. WHISPER__INFERENCE_DEVICE picks the device (note the double underscore).
  • WHISPER__TTL is how many seconds an idle model stays loaded (default 300). -1 keeps it loaded for the lowest delay.
  • LOG_LEVEL=info quiets the default debug logs.
  • The voice-activity filter is on by default and technically differs from the OpenAI spec. This helps with silence, but check it if you compare results with OpenAI.
  • faster-whisper itself is much faster than the original Whisper: its release notes mention 4x faster batched inference in v1.1.0.

FAQ#

Should I use the release candidate or the stable release? Use v0.8.3 unless you need diarization. This guide pins it for that reason.

Does it work on a Mac GPU? Not inside Docker. Use the CPU image. v0.8.2 added an Apple Silicon speedup, but how much it helps in a container is unclear (VERIFY).

Why does Open WebUI ask for a key? Open WebUI needs a non-empty value. Speaches only checks it if you set API_KEY, and this guide does.

Is the project still maintained? Issues #659 and #622 ask exactly that, and the last commit we saw is from April 2026. It works today. Keep your own backups and pin versions.

Which model for Japanese? Try Systran/faster-whisper-large-v3 with language=ja on a GPU, or Systran/faster-whisper-small on a CPU. Avoid the English-only distil models.

Sources#

Read on 2026-10-26:

Checked against the official docs and the v0.8.3 source on 2026-10-26. The commands were not run on a machine for this guide.