A self-hosted Discord music bot with a polished local dashboard, Discord-native control panels, persistent libraries, and Docker-first deployment.
Built for servers that want modern playback controls, strong persistence, and full ownership over the experience.
Quick Start · Features · Control Surfaces · Configuration · Development
Note
The bundled Docker Compose setup publishes the dashboard locally at http://127.0.0.1:3000 by default.
Last Music Bot combines the familiarity of a Discord music bot with the convenience of a dedicated local dashboard and the polish of Discord-native control panels. Playback is scoped per guild, library data is stored per user, and state persists across restarts so the bot feels consistent instead of disposable.
It is designed for personal servers and small communities that want:
- A web dashboard without relying on a hosted third-party service
- Rich queue and library management beyond basic slash commands
- Private, native Discord controls for day-to-day playback
- Persistent settings, history, playlists, and restore behavior
|
Queue actions, transport controls, connection state, and server settings stay scoped to the selected guild so one server does not interfere with another. |
Use Discord OAuth to sign in to a responsive dashboard with search, queue management, server switching, playlists, liked music, history, and settings. |
Control playback without leaving Discord through an invoker-only |
|
Store liked tracks, listening history, playlists, and user settings in SQLite with user-level isolation and persistent access across sessions. |
Resume from saved snapshots, honor saved guild defaults, remember preferred voice channels, and keep runtime playback state durable across restarts. |
Connect Spotify for private or collaborative playlist import, import supported public playlists without login when Spotify exposes them, and keep autoplay metadata available when credentials are configured. |
| Surface | Best For | What It Covers |
|---|---|---|
| Web dashboard | Full-session management | Search, queue editing, transport controls, liked tracks, history, playlists, per-user settings, guild settings |
/control |
Rich in-Discord control | Playback, queue browsing, search, liked/history access, playlists, save actions, volume |
/admin |
Server configuration | Command channel lock, preferred voice channel, announcements, autoplay, guild-level settings |
| Slash commands | Fast direct actions | Play, connect, pause/resume, skip, disconnect |
flowchart LR
U["Discord Users"] --> D["Slash Commands / Discord Panels"]
B["Browser Dashboard"] --> API["Express API"]
D --> BOT["Last Music Bot"]
API --> BOT
BOT --> LAV["Lavalink v4"]
BOT --> DB["SQLite Stores"]
BOT -. optional .-> SP["Spotify Catalog"]
Project layout:
apps/bot- bot runtime, REST API, player orchestration, stores, Discord commands, and panel controllersdashboard- static frontend served by the botlavalink- Lavalink configuration used by Docker Composedata- mounted persistence directory for SQLite databases
Core technologies:
discord.jsexpresslavalink-client- Lavalink v4
- SQLite
- Docker Compose
- TypeScript
Copy .env.example to .env.
Fill in the required values:
DISCORD_TOKENLAVALINK_PASSWORDSESSION_SIGNING_KEYandDATA_ENCRYPTION_KEYDISCORD_CLIENT_SECRETif you want dashboard login
Optional but recommended:
SPOTIFY_CLIENT_IDSPOTIFY_CLIENT_SECRETBACKUP_ROOT=/app/backups
In the Discord Developer Portal, add this redirect URI:
http://127.0.0.1:3000/api/auth/callback
If you change WEB_URL, your redirect URI must match it exactly:
${WEB_URL}/api/auth/callback
Spotify login is optional, but it is required for importing playlists you own or collaborate on.
In the Spotify Developer Dashboard, add this redirect URI:
${WEB_URL}/api/spotify/callback
Public playlist import can still work without login when Spotify exposes the playlist to the Web API, but some Spotify-managed or editorial playlists may still fail even when the URL is valid.
docker compose up --buildThe Compose stack now starts Lavalink, the bot, and the backup sidecar together. Lavalink still becomes healthy first, and the bot healthcheck remains available through docker compose ps.
http://127.0.0.1:3000
Sign in with Discord, choose a server, and start playback from the dashboard or from Discord commands.
The bot reads configuration from .env. The table below covers the primary runtime options.
| Variable | Required | Description |
|---|---|---|
DISCORD_TOKEN |
Yes | Discord bot token |
PROXY_MODE |
No | Proxy handling mode for client IP and rate limiting, defaults to direct for localhost use |
DISCORD_CLIENT_SECRET |
Yes for dashboard login | Used for Discord OAuth on the web dashboard |
DISCORD_CLIENT_ID |
No | Bot client ID; auto-detected if left blank |
DISCORD_GUILD_ID |
No | Preferred startup guild; auto-detected if left blank |
DISCORD_ALLOWED_VOICE_CHANNEL_IDS |
No | Comma-separated voice-channel allowlist |
LAVALINK_HOST |
Yes | Lavalink host, defaults to lavalink in Docker |
LAVALINK_PORT |
Yes | Lavalink port, defaults to 2333 |
LAVALINK_PASSWORD |
Yes | Shared Lavalink password |
SPOTIFY_CLIENT_ID |
No | Enables Spotify login, playlist import, and Spotify-backed autoplay metadata |
SPOTIFY_CLIENT_SECRET |
No | Enables Spotify login, playlist import, and Spotify-backed autoplay metadata |
SPOTIFY_MARKET |
No | Two-letter market code used for Spotify catalog access when app credentials are used, defaults to US |
WEB_HOST |
No | Dashboard bind host |
WEB_PORT |
No | Dashboard port |
WEB_URL |
Yes for OAuth correctness | Public dashboard URL used for callbacks and cookies |
SQLITE_PATH |
No | Player snapshot database path |
LIBRARY_SQLITE_PATH |
No | Library and settings database path |
SESSION_SIGNING_KEY |
Yes | Primary session-signing secret for dashboard authentication |
SESSION_SIGNING_KEY_PREVIOUS |
No | Temporary previous signing key used during rotation for OAuth/session compatibility |
DATA_ENCRYPTION_KEY |
Yes | Primary encryption secret for stored linked-account tokens |
DATA_ENCRYPTION_KEY_PREVIOUS |
No | Temporary previous encryption key used while rotating stored linked-account token encryption |
JWT_SECRET |
Deprecated fallback | Legacy session/encryption secret kept for compatibility during rollout |
BACKUP_ROOT |
No | Backup destination inside the container, defaults to /app/backups |
BACKUP_INTERVAL_MINUTES |
No | Scheduled backup interval for the backup sidecar, defaults to 60 |
BACKUP_HOURLY_RETENTION |
No | Number of hourly snapshots to retain, defaults to 48 |
BACKUP_DAILY_RETENTION |
No | Number of daily snapshots to retain, defaults to 14 |
AUDIT_LOG_RETENTION_DAYS |
No | Number of daily JSONL audit log files to retain, defaults to 30 |
WEB_URLmust match the exact browser URL used for dashboard access.- If
DISCORD_CLIENT_IDis blank, the bot resolves it automatically from the logged-in bot user. - If
DISCORD_GUILD_IDis blank, the bot auto-selects a startup guild from the servers it has joined. - If no voice allowlist is configured, the bot discovers available voice channels automatically.
- In production,
SESSION_SIGNING_KEYandDATA_ENCRYPTION_KEYshould never be left at sample values. - Set
SESSION_SIGNING_KEY_PREVIOUSandDATA_ENCRYPTION_KEY_PREVIOUStemporarily when rotating active secrets, then remove them after the migration window. JWT_SECRETcan be left unset after migration, but it can remain temporarily to preserve older sessions and linked Spotify credentials while you rotate secrets.- Switching from legacy JWT dashboard cookies to database-backed sessions causes a one-time dashboard re-login after deployment.
- Backups cover runtime user data only. They do not include
.envor other secret files.
Backups are written locally to ./backups on the host:
- hourly snapshots:
./backups/hourly/<timestamp-utc>/ - daily snapshots:
./backups/daily/<date-utc>/ - quick status pointer:
./backups/latest.json
Each backup contains:
library.dbplayer.dbrecommendation-track-index.jsonwhen presentmanifest.jsonwith checksums and source metadata
The backup sidecar creates snapshots every hour by default, keeps the newest 48 hourly backups, and keeps the newest 14 daily backups.
Manual one-shot backup:
docker compose run --rm backup node dist/scripts/backupData.js manualRestore a specific backup:
docker compose stop bot
docker compose run --rm backup node dist/scripts/restoreData.js <backup-id>
docker compose up -d botThe restore flow verifies the manifest first, creates a pre-restore safety snapshot, restores the tracked data files, and removes stale SQLite -wal and -shm files before the bot comes back up.
Sensitive actions are written as append-only JSONL audit records under ./data/audit/:
- Discord login success, cancel, failure, logout, and no-shared-guild denial
- Spotify connect, disconnect, and playlist import
- Playback control mutations and queue edits
- Guild settings changes
- Playlist create/delete/import
- Backup and restore operations
Audit logs rotate daily as YYYY-MM-DD.jsonl and are retained for 30 days by default.
Generate fresh secrets and a rollout checklist:
cd apps/bot
npm run build
npm run secrets:generateRecommended rotation flow:
- Move the current
SESSION_SIGNING_KEYintoSESSION_SIGNING_KEY_PREVIOUS. - Move the current
DATA_ENCRYPTION_KEYintoDATA_ENCRYPTION_KEY_PREVIOUS. - Set new values for
SESSION_SIGNING_KEYandDATA_ENCRYPTION_KEY. - Keep
JWT_SECRETonly as a temporary legacy fallback if you still need it. - Restart the stack, expect a one-time dashboard re-login, then remove the previous-key values after the migration window.
| Command | Description |
|---|---|
/play |
Search for a song or URL and add it to the queue |
/connect |
Join the voice channel the user is currently in |
/pause |
Toggle pause and resume for the current track |
/skip |
Skip the current track |
/disconnect |
Stop playback, leave voice, and clear the active session |
/control |
Open the private music control panel |
/admin |
Open the server admin panel |
The dashboard is served directly by the bot and communicates with the local REST API.
Main API groups:
- Auth:
/api/auth/login,/api/auth/callback,/api/auth/me,/api/auth/logout - Health:
/api/health - Spotify auth:
/api/spotify/login,/api/spotify/callback,/api/spotify/status,/api/spotify/disconnect - Playback:
/api/state,/api/connect,/api/disconnect,/api/queue,/api/play,/api/pause,/api/resume,/api/skip,/api/stop - Queue editing:
/api/queue/:index,/api/queue/:index/move-up,/api/queue/:index/move-down - Library:
/api/liked,/api/history,/api/playlists,/api/playlists/import/spotify - Settings:
/api/settings - Guilds:
/api/guilds,/api/guilds/:guildId/settings,/api/guilds/:guildId/channels
Guild-scoped playback routes accept an optional guildId query parameter so the dashboard can explicitly target the selected server.
Spotify support now covers two separate flows:
- Dashboard login and playlist import
- Autoplay enrichment and related-track lookup
Playlist import behavior in this repo:
- Public Spotify playlist URLs are supported only when Spotify exposes the playlist through app credentials.
- Private or collaborative playlists require linking a Spotify account through the dashboard.
- Imported playlists become local Last Music Bot playlists and playback still runs through Lavalink-resolved tracks.
- Re-import is append-only in v1: it adds newly seen occurrences but does not delete or reorder existing local items.
Autoplay is a guild-level toggle that keeps playback moving after the manual queue runs out.
Current behavior:
- Uses Spotify metadata when Spotify credentials are configured
- Preserves the effective requester on autoplayed tracks
- Applies per-session no-repeat safeguards
- Avoids retry loops when discovery fails
- Stops cleanly when no fresh candidate is available
Autoplay is intentionally conservative when discovery is incomplete, favoring a clean stop over unstable fallback behavior.
The Docker setup mounts the data directory so state survives container rebuilds.
Stored data includes:
player.db- player snapshots and playback restore statelibrary.db- liked tracks, history, playlists, user settings, and guild settings
The bot restores saved session state on startup and prefers Lavalink session resume when available.
The bundled Docker deployment includes container healthchecks:
- Lavalink is considered healthy when its local HTTP endpoint responds on port
2333 - The bot is considered healthy when
/api/healthreports both Discord and Lavalink as ready - The bundled bot healthcheck uses a dedicated short-lived bearer token instead of a dashboard session cookie
Use docker compose ps to inspect health, and docker compose logs -f bot lavalink if one service stays in starting or unhealthy.
If you want to work on the bot outside Docker, install the bot app dependencies first:
cd apps/bot
npm installUseful commands:
npm run dev
npm test
npm run buildIf you run locally without Docker, make sure Lavalink is already reachable at the configured host, port, and password.
The bot includes automated coverage for:
- Guild-aware API routing
- Discord command behavior
- Admin panel and control panel interactions
- Player persistence and restore logic
- Spotify catalog integration
- Autoplay and no-repeat behavior
- Dashboard smoke flows for playlist create/import, Spotify connect/disconnect, and mobile navigation
Run the suite from apps/bot:
npm test
npm run test:e2e
npm run test:smokeRecommended dashboard smoke checks after UI or auth changes:
- Create a playlist from search results, open it, and remove the imported item
- Open the dedicated
New Playlistmodal and confirm it lands in the playlist detail view - Open
Import Spotify Playlist, verify invalid URLs are rejected, then reconnect Spotify and finish an import - Verify the mobile nav drawer and playlist/import modals still behave correctly at widths at or below
768px
.
├─ apps/
│ └─ bot/
├─ dashboard/
├─ lavalink/
├─ data/
├─ docker-compose.yml
└─ .env.example
- The bundled Compose setup binds the dashboard to
127.0.0.1by default. - Global slash-command registration can take a short time to propagate after startup.
- The bot must be invited to at least one server before auto-detection can succeed.
- Spotify credentials are optional, but recommended if you want related-track autoplay.
Last Music Bot is a self-hosted, full-stack Discord music bot project that blends traditional slash-command playback with a dedicated local dashboard and polished Discord-native control panels. It is built for people who want a bot they can actually own, customize, and keep running with persistent state rather than starting from scratch every session.
