Skip to content
Controller

Self-Hosting Setup Guide ​

Run your own speech-to-text server, point the Diction app at it, start dictating. Your audio never touches our infrastructure.

Diction speaks the OpenAI transcription API (POST /v1/audio/transcriptions). Any server that implements it works. You don't need a GPU: the default engine runs on CPU, on anything from a cloud VM to a Raspberry Pi 4 or 5.

The quick start ​

You need Docker and about 2.5 GB of free memory. Clone the repo and start the default engine:

bash
git clone --depth 1 https://github.com/DictionLabs/Diction && cd Diction
docker compose --profile parakeet up -d

That's it. The engine's model is baked into the image, so there's nothing to download on first start. It covers 25 languages: English, Bulgarian, Croatian, Czech, Danish, Dutch, Estonian, Finnish, French, German, Greek, Hungarian, Italian, Latvian, Lithuanian, Maltese, Polish, Portuguese, Romanian, Slovak, Slovenian, Spanish, Swedish, Russian, and Ukrainian.

Now connect the app.

Need another language? Use Whisper ​

Whisper covers 99 languages. Same compose file, different profile, and tell the gateway which engine you started:

bash
DEFAULT_MODEL=small docker compose --profile small up -d

The first start takes a few minutes longer while the model downloads (a few hundred megabytes for small). It lands in a named volume, so later starts skip straight past it.

ProfileDEFAULT_MODELRAMNotes
smallsmall~850 MBRecommended starting point. Fast on CPU, fine for most dictations.
mediummedium~2.1 GBBetter with accents and background noise. Slow on CPU.
largelarge-v3-turbo~2.3 GBHighest accuracy. Slow on CPU, near-instant on an NVIDIA GPU.

DEFAULT_MODEL has to match the profile. If it names an engine that isn't running, every dictation fails. You can also put DEFAULT_MODEL=small in a .env file next to the compose file instead of typing it each time.

The Whisper models are our own CTranslate2 builds of OpenAI's checkpoints, published at huggingface.co/DictionLabs.

Have an NVIDIA GPU? The GitHub README has a ready-made GPU compose file for the large model.

The Diction gateway in this stack is fully open source. It runs as a proxy and streaming layer. It does not talk to our servers, does not require a subscription, and does not send any telemetry.

Connecting the app ​

The fast way: scan the QR code. Print it any time with:

bash
docker compose exec gateway gateway auth

That works whether the gateway just started or has been running for weeks. If you'd rather catch it at boot, docker compose logs gateway shows the same code, but only if you look before the logs scroll past it.

Open the Diction app, go to Self-Hosted, tap Scan to pair, and point the camera at your terminal. The URL and key land in the app in one step. Nothing to type, nothing to copy. Set PUBLIC_URL on the gateway (for example https://your-host:8080) so the QR carries the address your phone should use; without it the QR carries only the key and the app asks for the URL.

Requires Diction 13 or later and a current gateway image. On older versions, or if you'd rather type:

  1. Open the Diction app
  2. Switch to the Self-Hosted tab
  3. Paste your server URL into Gateway URL:
http://192.168.1.100:8080

Replace the address with your server's actual IP. Plain http:// is fine on your own network. Once connected, the Self-Hosted tab shows Speech model ready. Tap the mic and start dictating.

No public IP? ​

You don't need to open ports on your router. Several free options connect your phone to a home server from anywhere:

  • Cloudflare Tunnel. Free, outbound-only connection. No port forwarding.
  • Tailscale. Free WireGuard mesh VPN. Install on server and phone, connect from anywhere.
  • ngrok. Instant public URL. Great for quick testing.

Locking your gateway down ​

You put your gateway on a tunnel so you can dictate from anywhere. That means anyone who finds the URL can transcribe on your hardware. The gateway fixes this itself: it generates its own key on first start, prints it inside the pairing QR, and accepts it as a Bearer token on every request.

By default the key is advisory. Paired devices send it, but requests without it still pass, so upgrading the image never locks anyone out. Once your devices are paired, flip enforcement on:

yaml
  gateway:
    environment:
      DICTION_GATEWAY_AUTH: required

From that moment, requests without a valid key get a 401. Keep the gateway-data volume from the compose file mounted at /data; that's where the key lives. Without it a fresh key is generated on every container recreate and your devices must re-scan.

The key also rotates itself. A paired Diction app swaps in a fresh key on a regular schedule, old keys stay valid for a generous grace window so a phone that was off for a week catches up silently, and none of it needs you. If you'd rather manage the key by hand, set DICTION_GATEWAY_KEY to your own value; rotation turns off and the app treats it like any manual API key.

Make a leaked key expire ​

By default, a pairing key works forever once it's issued. That's fine for a home network, but if you ever tunnel your gateway to the internet, a key that leaked once (an old backup, a screenshot, a log you forwarded somewhere) stays valid indefinitely. Set an expiry and it doesn't:

yaml
  gateway:
    environment:
      DICTION_TOKEN_TTL: 2160h # 90 days

Your paired app refreshes its key on its own well before that window closes, so nothing breaks for a device you're actually using. Turning this on for the first time invalidates every key issued before it, so every paired device needs one re-scan. Let a device sit unused past its key's expiry and it'll ask you to re-scan too. That's the point: an expired key just means "prove you still have physical access to the gateway."

This only matters if you've also set DICTION_GATEWAY_AUTH: required above. Otherwise the gateway accepts requests without a key at all, so expiring the key protects nothing.

Want to see it work without waiting three months? Set DICTION_TOKEN_TTL: 2m temporarily and watch your app silently refresh its key roughly every minute in the container logs.

Optional: API key ​

If your server sits behind its own auth (common with reverse proxies or hosted endpoints), enter the key in the API Key field in the app's Self-Hosted settings. It's sent as a Bearer token with every request. Keys you type by hand are yours: the app never rotates or changes them.

Any Whisper endpoint works ​

Nothing here locks you to our containers. The Diction app and the gateway both talk the standard OpenAI transcription API. Anything that accepts POST /v1/audio/transcriptions with a file upload and returns a JSON transcript works:

  • Speaches (the engine behind dictionlabs/whisper-server)
  • whisper.cpp HTTP server
  • OpenAI's own Whisper API
  • Any future model that speaks the same protocol

Point the gateway at them rather than the app. The gateway names the model on every request, which the OpenAI spec requires and strict servers enforce. The app on its own leaves that field out, so a server that does not pin its own model will reject it. Configure a third-party backend with CUSTOM_BACKEND_URL and CUSTOM_BACKEND_MODEL, described in Use Your Own Model.

Already running one? See Use Your Own Model.

Requirements ​

  • Any machine that runs Docker, x86 or ARM: home server, NAS, cloud VM, Raspberry Pi 4 or 5. No GPU needed.
  • About 2.5 GB of free memory for the default engine.
  • iPhone on the same network, or reachable via tunnel or VPN

Full configuration ​

The complete compose file with multiple model profiles, and all gateway environment variables, is in the public GitHub repository.

AI features on self-hosted ​

Point the gateway at any OpenAI-compatible LLM with LLM_BASE_URL and LLM_MODEL, a local Ollama included, and you get the same Writing Tools as Diction Cloud:

  • Transcript cleanup. Filler words gone, punctuation fixed.
  • Edit by voice. Dictate an instruction and the gateway applies it to your text.
  • Suggestions. Tap a word and get two or three alternatives.

Paired devices get editing and suggestions right away. For anything else calling the gateway, set TEXT_ROUTES_OPEN=true, or turn on DICTION_GATEWAY_AUTH: required from Locking your gateway down. Results depend on the model: 7B or larger is recommended for editing, because smaller models often don't follow instructions reliably.