Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DiscordBot-CloudflareWarp
A small discord.js bot that runs entirely inside Docker and sends all of its
traffic through a Cloudflare WARP (Cloudflare One / Zero Trust client) tunnel. Every container runs its
own WARP client, so every bot gets its own VPN tunnel and its own Cloudflare egress IP. The bot proves it is
still on Cloudflare by logging the egress IP and warp=on flag on startup, on a timer and on demand.
Nothing runs on the host except Docker: npm install happens in the image build, and the lockfile was
generated inside a node:22-bookworm-slim container.
How it works
+-------------------------------------------------------------+
| container: discordbot-warp |
| |
| dbus -> warp-svc (Cloudflare WARP daemon, TUN device) |
| ^ |
| entrypoint.sh: register -> mode warp -> connect -> verify |
| | |
| node src/index.js ---- all traffic ----> WARP tunnel ---> Cloudflare ---> Discord
+-------------------------------------------------------------+
-
docker/entrypoint.shstartsdbusandwarp-svc, creates (or reuses) a WARP registration, setsmode warp, connects and waits untilwarp-cli statussaysConnected. -
It then calls
https://www.cloudflare.com/cdn-cgi/traceand refuses to start the bot unless Cloudflare reportswarp=on(orwarp=plus). The egress IP and colo are logged. -
bot/src/index.jslogs into Discord and everyWARP_CHECK_INTERVALseconds (default 60) asks Cloudflare once over IPv4 and once over IPv6 what it sees, logging a line like:[2026-09-08T00:20:15.123Z] [WARP] CONNECTED via Cloudflare (periodic) ipv4=104.28.x.x (warp=on) ipv6=2a09:bac1:xxxx::x:x (warp=on) colo=AMS loc=NL | warp-cli: Status update: Connected Network: healthyIf the tunnel drops it logs
NOT CONNECTEDand runswarp-cli connectto recover. -
A Docker
HEALTHCHECKperforms the samewarp=oncheck, sodocker psshowsunhealthyif the container ever falls off Cloudflare.
Slash commands
| Command | What it does |
|---|---|
/ping |
Replies with round-trip and gateway latency. |
/warp |
Runs a live tunnel check and replies with the Cloudflare IP, colo and status. |
Repository layout
bot/
package.json, package-lock.json Node project (discord.js 14)
src/index.js the bot
docker/
Dockerfile node:22-bookworm-slim + cloudflare-warp .deb
entrypoint.sh boots WARP, verifies, then execs the bot
docker-compose.yml one service per bot, each with its own WARP tunnel
scripts/deploy.sh ships the committed tree to the Docker host and restarts the stack
.env.example configuration template
CHANGELOG.md every change, newest first
Requirements
- A Docker host (tested on Debian 12 with Docker 20.10 and Compose v5) with
/dev/net/tunavailable. - Containers need
NET_ADMINand the TUN device. Both are set indocker-compose.yml. - A Discord bot token from the Developer Portal.
The Cloudflare WARP client is downloaded during the image build from
https://downloads.cloudflareclient.com/v1/download/bookworm-intel/version/<WARP_VERSION>
(WARP_VERSION defaults to 2026.7.1377.0, the Debian 12 build).
Quick start (on the Docker host)
git clone https://gitea.ikbengino.nl/ginoblij/DiscordBot-CloudflareWarp.git
cd DiscordBot-CloudflareWarp
cp .env.example .env # put your DISCORD_TOKEN in .env
docker compose up -d --build
docker compose logs -f
You should see the entrypoint report the tunnel, then the bot log in:
[entrypoint] warp-svc is up
[entrypoint] no registration found, creating a new one
[entrypoint] mode set to warp
[entrypoint] warp-cli status: Status update: Connected
[entrypoint] WARP tunnel verified: ip=104.28.x.x warp=on colo=AMS
[entrypoint] starting bot: node src/index.js
[bot] logged in as ... in 1 guild(s)
[WARP] CONNECTED via Cloudflare (startup) ipv4=104.28.x.x (warp=on) ipv6=2a09:bac1:xxxx::x:x (warp=on) colo=AMS loc=NL ...
Configuration (.env)
| Variable | Default | Meaning |
|---|---|---|
DISCORD_TOKEN |
Bot token. Required. | |
WARP_CHECK_INTERVAL |
60 |
Seconds between tunnel checks logged by the bot. |
GUILD_ID |
Register slash commands in one guild (instant) instead of globally. | |
WARP_MODE |
warp |
warp-cli mode to use (warp, doh, warp+doh, ...). |
WARP_CONNECT_TIMEOUT |
60 |
Seconds the entrypoint waits for Connected before giving up. |
Running more than one bot (one VPN per container)
Each service in docker-compose.yml is an independent container with its own warp-svc, its own
registration volume and therefore its own tunnel and egress IP. To add a bot:
- Create
.env.bot2with the second token. - Uncomment the
bot2service and thewarp-data-bot2volume indocker-compose.yml. docker compose up -d --build.
Deploying from this repo
scripts/deploy.sh sends the committed tree (git archive HEAD) to the host over SSH, rebuilds the
image there and restarts the stack. The .env on the host is never touched or overwritten.
scripts/deploy.sh # defaults to root@192.168.5.67:/opt/discordbot-cloudflarewarp
scripts/deploy.sh user@host /some/dir
Checking the tunnel by hand
docker compose exec bot warp-cli --accept-tos status
docker compose exec bot curl -s https://www.cloudflare.com/cdn-cgi/trace
docker compose logs -f | grep WARP
Change history
See CHANGELOG.md. Every change is committed and pushed with a changelog entry.