Records your Discord voice meetings, transcribes them locally with Whisper, and writes the minutes.
Website · Setup guide · Discord setup · Configuration · Run · API · MCP
Scriber is a self-hostable Discord meeting-recording bot. It joins a voice channel, records the conversation, transcribes it locally on your server with Whisper, and sends only the finished transcript to an AI provider of your choice (Anthropic, OpenAI, or any self-hosted OpenAI-compatible endpoint) to produce structured meeting minutes. A built-in Vue 3 admin dashboard lets you browse meetings, read transcripts and summaries, and change settings — all from one container.
- 🔴 One-command recording:
/scriber start,/scriber stop,/scriber cancel - 🗣️ Per-speaker transcription with timestamps — locally (faster-whisper, the default) or via a cloud STT engine (Mistral Voxtral, ElevenLabs Scribe, Google Chirp) with automatic fallback to local Whisper
- 🤖 Meeting minutes via Anthropic, OpenAI, or a self-hosted model (Ollama, vLLM, LM Studio, LocalAI)
- 📢 Automatic recording notice posted in the text channel so everyone knows recording started, what happens with the audio, and how to opt out (leave the channel)
- 🎧 Optional meeting audio keeping (on by default): play or download each meeting's recording from the dashboard, with an automatic expiration you control (default 30 days,
0= keep forever) - 🔁 Transcript regeneration: re-transcribe the saved audio with another engine — any local Whisper profile (
tiny…large-v3,turbo), Mistral Voxtral, ElevenLabs Scribe, or Google Chirp — and compare any two versions side by side - 🖥️ Admin dashboard: stats, meeting history, transcript/summary viewer and downloads, live settings
- 🔌 Token-authenticated REST API and an optional MCP server so your AI assistant can browse and edit everything Scriber stores
- 📦 Single container (Docker or Podman), all data in one
data/directory
/scriber start— you must be in a voice channel. Scriber joins it, starts recording, and posts a recording notice in the text channel where you invoked the command. Audio is segmented per speaker and transcribed while the meeting is running — locally with Whisper by default, or by the cloud engine you picked withTRANSCRIBE_ENGINE(the notice says which, and the bot falls back to local Whisper if the cloud API fails)./scriber stop— Scriber leaves the channel, finishes transcribing the remaining audio, writes the transcript to disk, sends it to the configured summary provider, and posts the resulting Markdown minutes in the text channel (with the summary file attached when it is long). If summarization fails, Scriber posts the raw transcript file instead so nothing is lost./scriber cancel— Scriber leaves the channel and discards everything recorded so far. No transcript is kept and nothing is sent anywhere.
Scriber also stops on its own — doing exactly what /scriber stop does
(leave, transcribe, summarize and post) — when either everyone else has left
the voice channel (it is alone) or no speech has been transcribed for 2 minutes.
If nothing was said, it just posts a short "no speech captured" note instead of
an empty summary.
- Docker or Podman (recommended), or Python 3.12+ with the
libopussystem library - A Discord application with a bot token (see below)
- An API key for Anthropic or OpenAI — or a self-hosted OpenAI-compatible server if you want summaries to never leave your machine
- CPU is fine for the
tiny/base/smallWhisper models; a CUDA GPU helps formedium/large-v3
Prefer a screenshot-guided, step-by-step version? See the web setup guide — it covers creating the app, finding your IDs, inviting the bot, and giving it access to a private voice channel.
-
Go to https://discord.com/developers/applications and click New Application. Give it a name, e.g.
Scriber. -
Open the Bot tab. Click Reset Token, copy the token, and put it in your
.envasDISCORD_TOKEN. Keep it secret. -
Privileged gateway intents: Scriber needs neither the Server Members Intent nor the Message Content Intent — you can leave them disabled.
-
Copy your Application ID from the General Information tab and invite the bot with this URL (replace
YOUR_APPLICATION_ID):https://discord.com/oauth2/authorize?client_id=YOUR_APPLICATION_ID&scope=bot%20applications.commands&permissions=1084416This uses the scopes
botandapplications.commands, and the permissions integer1084416covering: View Channels, Send Messages, Attach Files, Connect. -
Optional but recommended for testing: enable Developer Mode in your Discord client, right-click your server, Copy Server ID, and set it as
DISCORD_GUILD_IDin.env. Slash commands then appear instantly in that server instead of waiting for a global sync (which can take up to an hour).
Copy the example file and edit it:
cp .env.example .envThe two things you must set are DISCORD_TOKEN and at least one summary
provider block. All other keys have sensible defaults. Keys marked editable
in the table below can also be changed later from the dashboard settings
page — those edits are written back to your .env file.
Summaries can be generated by several providers configured as an ordered
list. Each provider is a numbered block of four keys (_1, _2, _3, …).
When a meeting ends, Scriber tries provider 1 first and, if it fails (HTTP
error, timeout, empty answer), automatically falls over to provider 2,
then 3, and so on until one succeeds. Only if every provider fails does the
bot report an error (and attach the raw transcript so nothing is lost).
Each block needs these keys, with <n> the provider's position in the chain:
| Key | Values |
|---|---|
SUMMARY_PROVIDER_<n> |
anthropic | openai | openai-compatible |
SUMMARY_API_KEY_<n> |
the provider's API key |
SUMMARY_MODEL_<n> |
the model ID |
SUMMARY_BASE_URL_<n> |
the provider's base URL |
Example: Anthropic first, OpenAI as a fallback, and a local model as a last resort.
# Provider 1: Anthropic (Claude) — tried first
SUMMARY_PROVIDER_1=anthropic
SUMMARY_API_KEY_1=sk-ant-xxxxxxxx
SUMMARY_MODEL_1=claude-opus-5
SUMMARY_BASE_URL_1=https://api.anthropic.com
# Provider 2: OpenAI (ChatGPT) — tried if provider 1 fails
SUMMARY_PROVIDER_2=openai
SUMMARY_API_KEY_2=sk-xxxxxxxx
SUMMARY_MODEL_2=gpt-4o
SUMMARY_BASE_URL_2=https://api.openai.com/v1
# Provider 3: self-hosted OpenAI-compatible (Ollama / vLLM / LM Studio /
# LocalAI) — optional last resort
SUMMARY_PROVIDER_3=openai-compatible
SUMMARY_API_KEY_3=none
SUMMARY_MODEL_3=llama3.1:8b
SUMMARY_BASE_URL_3=http://host.docker.internal:11434/v1Keep the numbering contiguous starting at 1. A single provider is fine —
just define the _1 block. You can also add providers from the dashboard
Settings page with the + Add provider button. With a self-hosted
provider, no data ever leaves your machine.
Legacy format: an old-style single provider without the numeric suffix (
SUMMARY_PROVIDER,SUMMARY_API_KEY, …) is still accepted and treated as provider1, so existing configs keep working. New setups should use the numbered format.
Set ADMIN_USERNAME and ADMIN_PASSWORD for the dashboard login. The
default password is change-me — change it before exposing port 8080 to
anyone else.
| Key | Default | Editable via dashboard | Purpose |
|---|---|---|---|
DISCORD_TOKEN |
— (required) | no | Discord bot token |
DISCORD_GUILD_ID |
(empty) | no | optional: restrict/speed up slash-command sync to one guild |
SUMMARY_PROVIDER_<n> |
anthropic (as _1) |
yes | provider kind of the n-th block: anthropic | openai | openai-compatible |
SUMMARY_API_KEY_<n> |
(empty) | yes | API key for provider n |
SUMMARY_MODEL_<n> |
claude-opus-5 (as _1) |
yes | model ID for provider n |
SUMMARY_BASE_URL_<n> |
https://api.anthropic.com (as _1) |
yes | base URL for provider n |
TRANSCRIBE_ENGINE |
whisper |
yes | live transcription engine: whisper (local) | voxtral | elevenlabs | google (cloud engines fall back to local Whisper on failure) |
WHISPER_MODEL |
base |
yes | tiny/base/small/medium/large-v3… |
WHISPER_LANGUAGE |
en |
yes | default language code or auto (overridable per meeting — see below) |
WHISPER_DEVICE |
cpu |
no | cpu/cuda |
WHISPER_COMPUTE_TYPE |
int8 |
no | int8/float16/… |
AUDIO_KEEP |
true |
yes | keep each meeting's audio (play/download + transcript regeneration) |
AUDIO_RETENTION_DAYS |
30 |
yes | days before kept audio is auto-deleted; 0 = never expire |
VOXTRAL_API_KEY |
(empty) | yes | Mistral API key — unlocks the Voxtral regeneration engine |
VOXTRAL_MODEL |
voxtral-mini-latest |
yes | Voxtral model ID |
VOXTRAL_BASE_URL |
https://api.mistral.ai |
yes | Mistral API base URL |
ELEVENLABS_API_KEY |
(empty) | yes | ElevenLabs API key — unlocks the Scribe regeneration engine |
ELEVENLABS_MODEL |
scribe_v2 |
yes | ElevenLabs speech-to-text model ID |
ELEVENLABS_BASE_URL |
https://api.elevenlabs.io |
yes | ElevenLabs API base URL |
GOOGLE_SPEECH_API_KEY |
(empty) | yes | Google Cloud API key — unlocks the Chirp regeneration engine |
GOOGLE_SPEECH_PROJECT |
(empty) | yes | Google Cloud project ID (required with the API key) |
GOOGLE_SPEECH_LOCATION |
eu |
yes | Speech-to-Text v2 region (eu, us, global, …) |
GOOGLE_SPEECH_MODEL |
chirp_3 |
yes | Google speech model ID |
ADMIN_USERNAME |
admin |
yes | dashboard login |
ADMIN_PASSWORD |
change-me |
yes | dashboard password |
WEB_HOST |
0.0.0.0 |
no | web server bind address |
WEB_PORT |
8080 |
no | web server port |
WEB_SECRET |
(empty) | no | HMAC key for session tokens; auto-generated and persisted to .env if empty |
MCP_ENABLED |
true |
no | MCP server on/off |
MCP_HOST |
0.0.0.0 |
no | MCP server bind address (same rules as WEB_HOST) |
MCP_PORT |
8081 |
no | MCP server port |
SCRIBER_DATA_DIR |
./data |
no | data directory (/data in the container) |
docker build -t scriber .
docker run -d --name scriber \
--env-file .env \
-v ./data:/data \
-v ./.env:/app/.env \
-p 8080:8080 \
-p 127.0.0.1:8081:8081 \
scriberPort 8081 is the optional MCP server — published on the
loopback only, so it stays reachable just from the machine itself. Drop that
line if you don't use it.
The .env bind mount lets settings changed in the dashboard persist on the
host; the data mount holds the database, transcripts and Whisper models.
Same commands, podman instead of docker:
podman build -t scriber .
podman run -d --name scriber \
--env-file .env \
-v ./data:/data:Z \
-v ./.env:/app/.env:Z \
-p 8080:8080 \
-p 127.0.0.1:8081:8081 \
scriberSELinux note: on Fedora/RHEL and other SELinux-enforcing systems, add the
:Zvolume label (as shown above) so the container is allowed to read and write the mounted files. Use lowercase:zinstead if the same directory is shared between multiple containers.
A compose.yaml is included:
docker compose up -d
# or
podman-compose up -dRebuild after pulling updates with docker compose up -d --build.
This is how the hosted instance at https://scriber.mydomain.com/ runs: a single
rootless Podman container named scriber, published only on the host
loopback, with nginx terminating TLS and reverse-proxying to it.
-
Build the image and run the container. Publishing on
127.0.0.1keeps the dashboard unreachable from the internet directly — only nginx can reach it.podman build -t scriber . podman run -d --name scriber \ --env-file .env \ -v ./data:/data:Z \ -v ./.env:/app/.env:Z \ -p 127.0.0.1:8885:8080 \ scriberTwo rules keep this from turning into a 502:
- In
.envkeepWEB_HOST=0.0.0.0(not127.0.0.1): a published port forwards to the container's own interface, so a loopback bind inside the container leaves the port unreachable. - Keep
WEB_PORT=8080— the port inside the container (the image's default). Pick the host port in the-pmapping instead:-p 127.0.0.1:8885:8080means "host8885→ container8080". If you bumpWEB_PORTto the host port without changing the mapping's container side, the app listens where the mapping doesn't point and the connection resets → 502.
The
127.0.0.1:in the-pmapping is what keeps the container private to nginx. Point nginx at the host port (8885). - In
-
Point nginx at the published port:
server { server_name scriber.mydomain.com; location / { proxy_pass http://127.0.0.1:8885; 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; } # TLS (certbot fills these in): # listen 443 ssl; # ssl_certificate /etc/letsencrypt/live/scriber.mydomain.com/fullchain.pem; # ssl_certificate_key /etc/letsencrypt/live/scriber.mydomain.com/privkey.pem; }
Reload nginx (
nginx -t && systemctl reload nginx) and browse to the domain.
To keep the container running across reboots on a rootless account, run
loginctl enable-linger <user> and generate a systemd unit with
podman generate systemd --new --name scriber.
A Discord problem never takes the web dashboard down, so you always get a page instead of a 502:
- If the bot is not invited or is missing permissions, Discord refuses the slash-command sync. The dashboard stays online; the login page shows a short notice and, once you sign in, the Dashboard shows the exact error with an Invite the bot to your server button using the correct scopes and permissions. Re-invite with that link, then restart the container.
- If
DISCORD_TOKENis unset or wrong, the dashboard still runs so you can fix it from the Settings page.
If you instead see a raw 502 Bad Gateway from nginx, the container itself is unreachable — see Troubleshooting.
| Command | Description |
|---|---|
/scriber start |
Join your current voice channel, post the recording notice, and start recording. One session per server at a time. |
/scriber start lang:<code> |
Same, but transcribe this meeting in a specific language (see below), overriding the configured default. |
/scriber stop |
Stop recording, finish the transcription, generate the summary and post it in the meeting's text channel. |
/scriber cancel |
Stop recording and discard everything — no transcript is kept, nothing is sent to any AI provider. |
The transcription language defaults to WHISPER_LANGUAGE from your .env
(the shipped .env.example sets it to en). Override it per meeting with the
optional lang option on /scriber start:
/scriber start lang:en # force English for this meeting
/scriber start lang:fr # force French
/scriber start lang:auto # auto-detect the language for this meeting
If you omit lang, the configured default is used. An unsupported code makes
/scriber start reply with an error (visible only to you) and no recording is
started.
Supported language codes (Whisper) — use auto for automatic detection,
or one of:
en English |
fr French |
de German |
es Spanish |
it Italian |
pt Portuguese |
nl Dutch |
ru Russian |
pl Polish |
uk Ukrainian |
zh Chinese |
ja Japanese |
ko Korean |
ar Arabic |
hi Hindi |
tr Turkish |
sv Swedish |
no Norwegian |
da Danish |
fi Finnish |
cs Czech |
el Greek |
he Hebrew |
hu Hungarian |
ro Romanian |
id Indonesian |
vi Vietnamese |
th Thai |
ca Catalan |
fa Persian |
Whisper recognizes about 100 languages in total; any of its ISO 639-1 codes
(plus yue Cantonese, haw Hawaiian, jw Javanese, and a few others) works.
The list above covers the most common ones.
Open http://localhost:8080 and sign in with ADMIN_USERNAME /
ADMIN_PASSWORD.
- Dashboard — stat cards (total meetings, completed, total duration, words transcribed, live active sessions, errors), a "meetings per day" chart for the last 30 days, and the meetings table. From each row you can view or download the transcript and the summary, download the kept meeting audio, inspect the meeting's processing log, and delete meetings (files and database row).
- Meeting page — play or download the meeting audio, edit the transcript
and summary, regenerate the transcript with another engine, and compare any
two transcript versions side by side (see
Meeting audio & transcript regeneration).
The Summary panel can also generate the summary again from any transcript
version: when summarization failed at
/scriber stopit shows a Retry summary button (fix the provider in Settings first, no need to re-record), and on a finished meeting Regenerate summary replaces the minutes — handy after editing or regenerating the transcript. Two switches: Refresh participant memory (on by default only when the meeting never had a summary, since memory is normally refreshed once at/scriber stop; turn it on to update the memory files from a better summary) and Post it to the Discord channel. - Settings (⚙️) — edit the dashboard-editable configuration keys: the
summary provider failover list (provider kind, API key, model and base URL
for each, plus a + Add provider button to extend the chain), the Whisper
model and default language, meeting audio keeping and its expiration, the
transcription engine API keys, and the admin credentials. Secret values are
masked; changes are written back to
.envand take effect immediately for the next meeting. GET /api/healthis available without authentication for monitoring and returns the bot connection status plus a shortnoticewhen the bot needs attention (used to warn on the login page before you sign in).
With AUDIO_KEEP enabled (the default), Scriber stores each meeting's audio
under data/audio/:
- a playable mix (
<meeting-id>.ogg) — every speaker placed at their real time offset, playable and downloadable from the dashboard (meeting page and meetings table); - a compact segment track + index (
<meeting-id>.segments.ogg/.segments.json) — the raw per-speaker speech segments, which is what makes regeneration below possible while keeping speaker attribution.
Audio expires automatically after AUDIO_RETENTION_DAYS days (default 30;
0 disables expiration entirely — audio is kept forever). Only audio expires:
transcripts and summaries are always kept. Encoding uses ffmpeg (bundled
in the container image); when ffmpeg is missing, Scriber falls back to
uncompressed WAV files, which work the same but are much larger.
On a meeting's page (when its audio is kept), pick an engine + model and hit Regenerate transcript. The archived segments are re-transcribed and the result is stored as a new transcript version next to the existing ones — nothing is overwritten. Available engines:
| Engine | Runs | Needs |
|---|---|---|
| Whisper (local) | on your machine | nothing — pick any profile: tiny, base, small, medium, large-v3, large-v3-turbo, distil-large-v3, … |
| Voxtral (Mistral AI) | cloud | VOXTRAL_API_KEY |
| ElevenLabs Scribe | cloud | ELEVENLABS_API_KEY |
| Google Chirp (Speech-to-Text v2) | cloud | GOOGLE_SPEECH_API_KEY + GOOGLE_SPEECH_PROJECT |
Cloud keys are configured in Settings (or .env) and audio segments are
only sent to a cloud engine when you explicitly regenerate with it. Cloud
rate limits are handled with retries and backoff, so a long meeting on a
rate-limited plan simply takes longer. Once a meeting has two or more
versions, Compare side by side opens a split view with a version picker
above each pane — handy for judging which engine/model transcribes your
meetings best.
One version is the meeting's main transcript (badge Main): the one shown in the editor, served by the API and MCP, and used by default to generate the summary. It is the live recording at first; Use as main switches to a better version (the previous main is kept as a version), and any version — the live original included — can be deleted as long as one remains. Deleting the main one promotes the most recent remaining version.
Scriber exposes a token-authenticated REST API for reading (and optionally writing) everything it stores — meetings, transcripts, summaries, participants and memories — so you can plug it into your own scripts and integrations.
-
Create a token in the dashboard under Settings → API access. Give it a name and a scope —
read(GET only) orread & write(GET + edits). The token is a 48-character alphanumeric secret shown once; Scriber stores only its SHA-256 hash. Delete a token to revoke it immediately. -
Call the API under
/api/v1on the same host as the dashboard, sending the token as a bearer credential:curl -H "Authorization: Bearer <token>" https://your-host/api/v1/meetings
401 means the token is missing or invalid; 403 means a read-only token tried
a write. Key endpoints (see GET /api/v1/ for the full list):
| Method & path | Scope | Purpose |
|---|---|---|
GET /api/v1/me |
read | Info about the calling token |
GET /api/v1/stats |
read | Aggregate statistics |
GET /api/v1/meetings |
read | Paginated meeting list |
GET /api/v1/meetings/{id} |
read | One meeting incl. log |
GET /api/v1/meetings/{id}/transcript |
read | Transcript text |
GET /api/v1/meetings/{id}/transcripts |
read | Transcript versions (original + regenerated) |
GET /api/v1/meetings/{id}/transcripts/{tid} |
read | One version's text (tid = original or a version id) |
GET /api/v1/meetings/{id}/summary |
read | Summary Markdown |
GET /api/v1/meetings/{id}/audio |
read | Kept meeting audio (Ogg/Opus, ?download=1 for attachment) |
GET /api/v1/participants |
read | Paginated participant list |
GET /api/v1/participants/{id} |
read | Participant + memory + sessions |
GET /api/v1/participants/{id}/memory |
read | Memory Markdown |
GET /api/v1/participants/{id}/avatar |
read | Avatar image |
PUT /api/v1/meetings/{id}/transcript |
read & write | Overwrite transcript |
PUT /api/v1/meetings/{id}/summary |
read & write | Overwrite summary |
PUT /api/v1/participants/{id} |
read & write | Update name/description |
PUT /api/v1/participants/{id}/memory |
read & write | Overwrite memory |
Full reference with curl examples: the
API documentation.
Scriber also exposes its stored data over the Model Context Protocol, so AI assistants (Claude Code, Claude Desktop, Cursor, …) can browse and edit meetings, transcripts, summaries, participants and memories through purpose-built tools instead of raw HTTP calls.
- Endpoint: streamable HTTP at
http://127.0.0.1:8081/mcp(portMCP_PORT, default8081). - Enabled by default, but only reachable where you publish its port — the
compose file and the run examples above bind it to
127.0.0.1, so it stays private to the machine. SetMCP_ENABLED=falsein.envto switch the listener off entirely. - Authentication: the same API tokens as the REST API (dashboard →
Settings → API access), sent as
Authorization: Bearer <token>. Read tools accept any token; theupdate_*tools need theread & writescope.
Add it to Claude Code:
claude mcp add --transport http scriber http://127.0.0.1:8081/mcp \
--header "Authorization: Bearer <token>"Or in any client that takes a JSON server config:
{
"mcpServers": {
"scriber": {
"type": "http",
"url": "http://127.0.0.1:8081/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}Tools mirror the REST API: get_stats, list_meetings, get_meeting,
get_transcript, get_summary, list_participants, get_participant and
get_participant_memory to read; update_transcript, update_summary,
update_participant and update_participant_memory to edit (read & write
scope).
Paste this into your assistant's instructions (CLAUDE.md, system prompt, …):
You have a "scriber" MCP server for a Discord meeting-recording bot (if MCP
is unavailable, the same data is at https://YOUR-SCRIBER-HOST/api/v1 with
"Authorization: Bearer <token>"). Use it to:
- find meetings (list_meetings) and read their transcript/summary
(get_transcript, get_summary) when asked about past discussions;
- read per-participant memory files (get_participant_memory) before writing
about people, so names and project terms stay correct;
- update those memories (update_participant_memory) when you learn durable
facts about a participant — keep them short, markdown, third-person.
The model is downloaded automatically on first use into data/models/.
Rough guidance for CPU with int8 compute:
| Model | Download | RAM (approx.) | Speed | Quality |
|---|---|---|---|---|
tiny |
~75 MB | ~0.5 GB | fastest | okay for clear speech |
base |
~145 MB | ~0.7 GB | fast | good default |
small |
~500 MB | ~1.5 GB | moderate | noticeably better |
medium |
~1.5 GB | ~3 GB | slow on CPU | very good |
large-v3 |
~3 GB | ~5 GB | GPU recommended | best |
- When a recording starts, Scriber posts a recording notice in the text channel naming the voice channel, the transcription engine in use (and, for a cloud engine, that speech audio is sent to that service), and the exact external AI target (host and model) the transcript will be sent to — so every participant can leave the channel before being recorded.
- With the default engine, audio is transcribed locally (Whisper) and no
audio leaves your machine during the meeting. If you opt into a cloud
engine (
TRANSCRIBE_ENGINE), speech segments are sent to that provider while the meeting runs — announced in the recording notice. - Meeting audio is kept on your server when
AUDIO_KEEPis on (the default) so you can replay it and regenerate transcripts; it expires automatically afterAUDIO_RETENTION_DAYS(0= keep forever) and is never sent anywhere unless you explicitly regenerate with a cloud engine. SetAUDIO_KEEP=falseto store text only, like earlier versions. - The final meeting transcript is sent once to the summary provider you configured — and if you use a self-hosted OpenAI-compatible provider, not even that.
/scriber canceldiscards all data of the current recording — audio included; nothing is stored or sent.- Everything Scriber stores lives in the
data/directory:scriber.db(meeting metadata and logs, SQLite),transcripts/(transcript and summary files),audio/(kept meeting audio), andmodels/(downloaded Whisper models). Delete a meeting from the dashboard to remove its files and database row.
- nginx shows
502 Bad Gateway/ the page never loads — the container itself is unreachable, not a bot problem. Check, in order: the container is running (podman ps);WEB_HOST=0.0.0.0in.env(a127.0.0.1bind makes a published port unreachable); the container side of the-pmapping matchesWEB_PORT(both8080in the example — a-p …:8080publish withWEB_PORT=8885resets the connection because nothing listens on 8080); the host side of the mapping matches your nginxproxy_pass(both8885); and the container logs (podman logs scriber) for a startup error. Once the container answers, a bot that is not invited shows an in-page notice, not a 502. - The bot does not join the voice channel — check that the bot has the
View Channels and Connect permissions on that specific voice channel,
and that you are in a voice channel of the same server when running
/scriber start. - Slash commands do not show up — global command sync can take up to an
hour. Set
DISCORD_GUILD_IDto your server's ID for instant sync, and make sure the bot was invited with theapplications.commandsscope. - First
/scriber startis slow — the Whisper model is downloaded on first use. Watch the container logs; subsequent meetings reuse the cached model indata/models/. - Transcription is slow or the summary arrives late — try a smaller
WHISPER_MODEL(e.g.tinyorbase), or setWHISPER_DEVICE=cudawithWHISPER_COMPUTE_TYPE=float16if you have an NVIDIA GPU (requires a CUDA-enabled container runtime). - Summary fails with an error — the transcript is never lost: Scriber
attaches the raw transcript file to the error message in Discord, and it
stays available in the dashboard. Check
SUMMARY_API_KEY,SUMMARY_MODELandSUMMARY_BASE_URLon the settings page, then open the meeting in the dashboard and click Retry summary. - A new API key in
.envis ignored (the old key's error keeps coming back) — when.envis bind-mounted as a single file (-v ./.env:/app/.env), the container is attached to that file's inode. Editors and tools that save by replacing the file (vim,sed -i, most config-management tools) create a new inode, so the container keeps reading the old content until it is restarted. Either change keys from the dashboard Settings page (applied live, written in place), or restart the container after editing.envon the host. /scriber stopanswers "There is no active recording session" while a recording is running (and the logs showUnknown interactionorInteraction has already been acknowledged) — two Scriber processes are logged in with the sameDISCORD_TOKEN(an old host that was never shut down, a forgotten dev instance…). Discord delivers each slash command to both; the instance that is not holding the recording can answer first. Find and stop the duplicate — or reset the bot token in the Discord Developer Portal and set the new one only on the instance you keep.- Dashboard login fails — credentials are
ADMIN_USERNAME/ADMIN_PASSWORDfrom your.env. If you changed them via the dashboard, the new values are in the mounted.envfile.
Released under the MIT License.