A Discord music bot powered by Lavalink. Simple to deploy, easy to use.
- Features
- Quick Start
- Commands
- Configuration
- Managing the Bot
- Troubleshooting
- Built With
- Contributing
- Links
- License
- Play music from YouTube, SoundCloud, Bandcamp, Twitch, and Vimeo
- Optional Spotify support (search and resolve via YouTube)
- Autoplay mode for continuous music playback
- Queue management with shuffle, loop, and play-next
- Interactive search with track selection
- Deploy-time language selection (English, Spanish, Turkish, Italian, Brazilian Portuguese)
- Role-based access control
- Runs entirely in Docker, no host dependencies
- Works without a self-hosted Lavalink server (automatic public server fallback)
- A Discord bot token from the Developer Portal
- Docker installed
When creating your bot, enable all 3 Privileged Gateway Intents (Presence, Server Members, Message Content).
Uses the pre-built GHCR image, no cloning needed.
1. Create a project directory:
mkdir beatdock && cd beatdock2. Create .env:
TOKEN=your_discord_bot_token3. Create docker-compose.yml:
services:
bot:
container_name: beatdock
image: ghcr.io/albertgmz/beatdock:latest
depends_on:
lavalink:
condition: service_healthy
networks:
- beatdock-network
env_file: .env
lavalink:
container_name: beatdock-lavalink
image: ghcr.io/lavalink-devs/lavalink:4
ports:
- "2333:2333"
networks:
- beatdock-network
volumes:
- ./application.yml:/opt/Lavalink/application.yml:ro
environment:
- LAVALINK_PASSWORD=${LAVALINK_PASSWORD:-youshallnotpass}
- SPOTIFY_ENABLED=${SPOTIFY_ENABLED:-false}
- SPOTIFY_CLIENT_ID=${SPOTIFY_CLIENT_ID:-}
- SPOTIFY_CLIENT_SECRET=${SPOTIFY_CLIENT_SECRET:-}
healthcheck:
test: ["CMD", "/bin/bash", "-c", "echo > /dev/tcp/localhost/2333"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# Mints the YouTube poToken on your own IP (credential-free). See "YouTube poToken auto-refresh".
bgutil-provider:
container_name: beatdock-bgutil
image: brainicism/bgutil-ytdlp-pot-provider:1.3.1
init: true
networks:
- beatdock-network
# Pushes a fresh poToken to Lavalink's POST /youtube hot-swap route on a loop.
pot-refresher:
container_name: beatdock-pot-refresher
image: alpine:3
init: true
depends_on:
lavalink:
condition: service_healthy
bgutil-provider:
condition: service_started
networks:
- beatdock-network
volumes:
# Long syntax: fail loudly if the script is missing instead of creating a junk dir.
- type: bind
source: ./scripts/pot-refresher.sh
target: /pot-refresher.sh
read_only: true
bind:
create_host_path: false
environment:
- LAVALINK_PASSWORD=${LAVALINK_PASSWORD:-youshallnotpass}
- POT_REFRESH_INTERVAL=${POT_REFRESH_INTERVAL:-1800}
command: sh -c "apk add --no-cache curl jq >/dev/null && exec sh /pot-refresher.sh"
networks:
beatdock-network:
name: beatdock_network4. Create application.yml:
server:
port: 2333
address: 0.0.0.0
plugins:
youtube:
enabled: true
allowSearch: true
allowDirectVideoIds: true
allowDirectPlaylistIds: true
clients:
- MUSIC
- WEB
- WEBEMBEDDED
- ANDROID_VR
lavalink:
plugins:
- dependency: "dev.lavalink.youtube:youtube-plugin:1.18.1"
snapshot: false
server:
password: "${LAVALINK_PASSWORD:youshallnotpass}"
sources:
youtube: false
soundcloud: true
bandcamp: true
twitch: true
vimeo: true
http: false
local: false
bufferDurationMs: 200
frameBufferDurationMs: 1000
youtubePlaylistLoadLimit: 3
playerUpdateInterval: 2
trackStuckThresholdMs: 5000
useSeekGhosting: true
ratelimit:
retryLimit: 5
logging:
level:
root: INFO
lavalink: INFO5. Add the poToken refresher script:
Download scripts/pot-refresher.sh into a scripts/ folder next to your docker-compose.yml. It mints a fresh YouTube poToken and hot-pushes it to Lavalink (see YouTube poToken auto-refresh).
6. Deploy:
docker compose up -dPlaying arbitrary (non-music) videos and autoplay/RD-mix recommendations requires YouTube's WEB client, which needs a short-lived poToken. BeatDock keeps this fully automated and credential-free β no Google account, no API key, no manual pasting:
bgutil-providermints a{poToken, visitorData}pair on your server's own IP.pot-refresherpushes it to Lavalink'sPOST /youtubehot-swap route (no restart) and re-pushes everyPOT_REFRESH_INTERVALseconds (default1800), covering both token expiry and Lavalink restarts.
Music keeps working through the MUSIC/ANDROID_VR clients even if the refresher is temporarily down. Age-restricted videos may still be unavailable.
Note: this only applies to the self-hosted stack. If BeatDock connects to public Lavalink nodes (no
LAVALINK_HOSTset), tokens are managed by each node's operator, so thebgutil-providerandpot-refresherservices are not used and non-music playback depends on the node.
git clone https://github.com/albertgmz/BeatDock.git
cd BeatDockCreate .env with your credentials (see .env.example for all options):
TOKEN=your_discord_bot_tokendocker compose up -dBeatDock can run without a self-hosted Lavalink server. If LAVALINK_HOST, LAVALINK_PORT, and LAVALINK_PASSWORD are not set, the bot automatically fetches free public Lavalink v4 servers and connects to one. User search queries and track requests are sent to the selected public node. Set PUBLIC_NODE_HOST_ALLOWLIST if you only trust specific public Lavalink hosts.
On public nodes the YouTube poToken auto-refresh does not apply: each node manages its own tokens, so whether non-music videos and autoplay mixes play depends on that node's setup.
To use public servers, simply comment out the Lavalink variables in your .env:
# LAVALINK_HOST=lavalink
# LAVALINK_PORT=2333
# LAVALINK_PASSWORD=youshallnotpass| Command | Description |
|---|---|
/play <query> [next] |
Play a song (optionally add to front of queue) |
/search <query> |
Search and select tracks |
/pause |
Pause/resume |
/skip |
Skip track |
/back |
Previous track |
/stop |
Stop and disconnect |
/queue |
Show queue |
/shuffle |
Shuffle queue |
/autoplay |
Toggle autoplay mode |
/loop |
Toggle loop mode |
/clear |
Clear queue |
/volume <1-100> |
Set volume |
/lyrics |
Show lyrics for the current song |
/filter |
Apply audio effects and EQ presets |
/nowplaying |
Current track info |
/invite |
Get bot invite link |
/about |
Bot info |
All configuration is done through the .env file. Only TOKEN is required.
| Variable | Default | Description |
|---|---|---|
TOKEN |
- | Discord bot token (required) |
SPOTIFY_ENABLED |
false |
Enable Spotify search support |
SPOTIFY_CLIENT_ID |
- | Spotify app client ID |
SPOTIFY_CLIENT_SECRET |
- | Spotify app client secret |
DEFAULT_LANGUAGE |
en |
Global bot language for this deployment (en, es, tr, it, pt-BR) |
DEFAULT_VOLUME |
80 |
Default playback volume (0-100) |
AUTOPLAY_DEFAULT |
false |
Enable autoplay by default when music starts |
ALLOWED_ROLES |
- | Comma-separated role IDs to restrict access |
DEFAULT_SEARCH_PLATFORM |
ytmsearch |
Default search platform for user queries |
LAVALINK_PASSWORD |
youshallnotpass |
Lavalink server password |
PUBLIC_NODE_HOST_ALLOWLIST |
- | Optional comma-separated host or *.domain allowlist for public Lavalink fallback |
QUEUE_EMPTY_DESTROY_MS |
30000 |
Disconnect after queue empties (ms) |
EMPTY_CHANNEL_DESTROY_MS |
60000 |
Disconnect from empty channel (ms) |
docker compose logs -f # View logs
docker compose restart # Restart
docker compose down # Stop
docker compose pull && docker compose up -d # UpdateRaspberry Pi 5 (Debian 13) may use a 16KB memory page size, which is incompatible with Lavalink's DAVE encryption library. Check with:
getconf PAGE_SIZEIf the result is not 4096, add kernel=kernel8.img under the [all] section in /boot/firmware/config.txt, then reboot and restart the containers. See #109 for details.
- discord.js - Discord API client
- Lavalink - Audio player server
- lavalink-client - Lavalink client library
- Docker - Containerized deployment
- Node.js 22+ - Runtime
Contributions are welcome. Bug fixes, new features, translations, docs - all good. Check the guide below to get started.
See CONTRIBUTING.md for setup instructions and guidelines.