npm install
for development
npm run dev
for production
npm run start
Set COMMAND_PREFIX in your environment if you do not want the default ! prefix.
Available commands:
!play <youtube url or search>!skip!stop!pause!resume!volume <0-200>!queue!nowplaying!commands!version
The bot joins the caller's voice channel as soon as !play arrives and stays 30 seconds after
the queue ends. It leaves before the next song if nobody else is in the channel, and !stop
makes it leave right away. Downloaded songs are cached on disk (up to 1 GB) and lookups in
memory, so repeats start instantly.
!version shows app version and, when available, deploy metadata (RELEASE_VERSION and GIT_SHA).
By default the per-guild volume set with !volume lives in memory only and resets when the
process restarts. On Fly the Machine is destroyed when scaled to zero (overnight) and rebuilt
on deploy, so in-memory state is lost on both — and a Fly volume would add storage cost.
To persist volume for free, point the bot at a free-tier Upstash Redis (serverless, accessed over HTTPS — no extra npm dependency). Set both REST credentials:
UPSTASH_REDIS_REST_URL=...
UPSTASH_REDIS_REST_TOKEN=...
On Fly, set them as secrets on each app:
flyctl secrets set UPSTASH_REDIS_REST_URL=... UPSTASH_REDIS_REST_TOKEN=... -a manibot
flyctl secrets set UPSTASH_REDIS_REST_URL=... UPSTASH_REDIS_REST_TOKEN=... -a manibot-dev
Keep dev and prod state separate. If manibot and manibot-dev share a Discord server
(same guild IDs) and the same Redis, they will overwrite each other's volume. Either give each
app its own Upstash database, or point them at different hash keys with MANIBOT_VOLUME_KEY
(e.g. manibot:volumes for prod, manibot-dev:volumes for dev).
When the variables are unset the bot keeps its previous in-memory behaviour, and any Redis
outage is logged and ignored — it never blocks the !volume command or startup.
Every push to main makes CI publish ghcr.io/manivela/manibot:latest. deploy/compose.yaml
runs it next to an updater that pulls latest every 5 minutes and recreates the bot when the
image or .env changed. Nothing is exposed to the internet; secrets live only in .env.
Once, on GitHub: make the manibot package public (Packages → manibot → Package settings →
Change visibility) so the host can pull it without credentials.
On the host (x86-64; at least 4 CPUs or lower cpus in compose.yaml):
- Put
deploy/compose.yamlin an empty folder with a.env(notsample.env, which setsNODE_ENV=development):BOT_TOKEN= UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN= MANIBOT_VOLUME_KEY= SENTRY_DSN= APP_ENV= COMMAND_PREFIX=! docker compose up -d, thendocker compose logs -f botuntilBot is ready.
On Windows, Docker Desktop must start on sign-in, and the bot runs only while a user is
signed in. deploy/run.bat, placed next to compose.yaml, does step 2 on a double-click.
Deploy: push to main; it is live within ~5 minutes of CI finishing (!version shows the
commit). Logs: Sentry (below), or docker compose logs --tail 100 bot.
With SENTRY_DSN set and NODE_ENV=production, the bot sends errors, logs (everything it
prints, one Playing … line per track, a warning per failed command), a trace per !play and
per track start, and its memory/CPU every minute. Off Fly it also counts a heartbeat metric
every 5 minutes while connected to Discord; a Sentry metric monitor emails when none arrives
for 15 minutes.
CircleCI deploys main to Fly and sets runtime secrets used by !version.
Required CircleCI project env var:
FLY_API_TOKEN
You can run a separate development bot on Fly using fly.dev.toml.
Create the dev app once:
flyctl apps create manibot-dev
Set secrets for the dev app (use a separate Discord bot token):
flyctl secrets set BOT_TOKEN=... -a manibot-dev
flyctl secrets set COMMAND_PREFIX=! -a manibot-dev
Deploy dev:
npm run deploy:fly:dev
View dev logs:
npm run logs:fly:dev
GitHub Actions stop your production Machines overnight and start them again in the morning so you mostly avoid CPU/RAM charges while offline.
Current defaults (Turkey TRT, UTC+3, no DST):
- Stop: 01:00 Turkey →
0 22 * * *UTC (fly-schedule-stop.yml) - Start: 08:00 Turkey →
0 5 * * *UTC (fly-schedule-start.yml)
Edit those cron lines if you change hours (always convert local → UTC for the workflow).
Setup
-
In the GitHub repo: Settings → Secrets and variables → Actions → New repository secret
- Name:
FLY_API_TOKEN - Value: a Fly token with permission to manage the app (same idea as CircleCI deploy).
- Name:
-
Optionally adjust start time in
fly-schedule-start.yml(default wake 08:00 Turkey). -
Actions tab → run Fly — stop manibot / Fly — start manibot manually once to verify.
Notes
- Stopped Machines can still incur a small rootfs charge; running 24/7 is what costs most.
- A deploy from CircleCI may start or replace Machines depending on Fly behavior; after a deploy, check the schedule still matches what you want.
- For
manibot-dev, duplicate these workflows and changeFLY_APP(or we can add a matrix later).
If YouTube blocks playback on hosted IPs, set YTDLP_COOKIES as a secret.
- Use exported
youtube.comcookies in Netscapecookies.txtformat - Do not use a raw HTTP
Cookie:header string - Prefer a fresh export from an incognito/private YouTube session per yt-dlp wiki guidance