Initial Discord bot in Docker behind a per-container Cloudflare WARP tunnel
- discord.js 14 bot with /ping and /warp, periodic WARP egress-IP logging - Dockerfile: node:22-bookworm-slim + cloudflare-warp 2026.7.1377.0 (.deb) - entrypoint boots dbus/warp-svc, registers, connects, verifies warp=on - docker-compose with NET_ADMIN, /dev/net/tun, sysctls, healthcheck - deploy script, README, CHANGELOG, lockfile generated inside Docker Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 1: install Node dependencies (npm runs only inside Docker)
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM node:22-bookworm-slim AS deps
|
||||
WORKDIR /app
|
||||
COPY bot/package.json bot/package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 2: runtime image = Node 22 on Debian 12 (bookworm) + Cloudflare WARP
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
ARG WARP_VERSION=2026.7.1377.0
|
||||
ARG WARP_DEB_URL=https://downloads.cloudflareclient.com/v1/download/bookworm-intel/version/${WARP_VERSION}
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
NODE_ENV=production
|
||||
|
||||
# dbus + iproute2 are required by warp-svc; curl for health checks.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl gnupg dbus iproute2 procps \
|
||||
&& curl -fsSL "${WARP_DEB_URL}" -o /tmp/cloudflare-warp.deb \
|
||||
&& apt-get install -y --no-install-recommends /tmp/cloudflare-warp.deb \
|
||||
&& rm -f /tmp/cloudflare-warp.deb \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& warp-cli --version
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY bot/package.json ./package.json
|
||||
COPY bot/src ./src
|
||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
# WARP registration/state lives here; mount a volume per container to keep it.
|
||||
VOLUME ["/var/lib/cloudflare-warp"]
|
||||
|
||||
HEALTHCHECK --interval=60s --timeout=15s --start-period=90s --retries=3 \
|
||||
CMD curl -fsS --max-time 10 https://www.cloudflare.com/cdn-cgi/trace | grep -Eq '^warp=(on|plus)$' || exit 1
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
CMD ["node", "src/index.js"]
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
# Boots Cloudflare WARP inside this container, verifies the tunnel is up and
|
||||
# that Cloudflare sees us as a WARP client, then hands over to the bot.
|
||||
set -euo pipefail
|
||||
|
||||
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [entrypoint] $*"; }
|
||||
|
||||
WARP_MODE="${WARP_MODE:-warp}"
|
||||
WARP_CONNECT_TIMEOUT="${WARP_CONNECT_TIMEOUT:-60}"
|
||||
TRACE_URL="https://www.cloudflare.com/cdn-cgi/trace"
|
||||
|
||||
cleanup() {
|
||||
log "shutting down"
|
||||
warp-cli --accept-tos disconnect >/dev/null 2>&1 || true
|
||||
kill "${WARP_SVC_PID:-}" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# --- 1. system dbus (warp-svc talks to warp-cli over it) --------------------
|
||||
mkdir -p /run/dbus
|
||||
rm -f /run/dbus/pid
|
||||
if ! pgrep -x dbus-daemon >/dev/null; then
|
||||
dbus-daemon --system --fork
|
||||
log "dbus started"
|
||||
fi
|
||||
|
||||
# --- 2. WARP daemon ---------------------------------------------------------
|
||||
log "starting warp-svc ($(warp-cli --version | head -n1))"
|
||||
warp-svc >/var/log/warp-svc.log 2>&1 &
|
||||
WARP_SVC_PID=$!
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
if warp-cli --accept-tos status >/dev/null 2>&1; then break; fi
|
||||
if ! kill -0 "$WARP_SVC_PID" 2>/dev/null; then
|
||||
log "warp-svc exited early:"; cat /var/log/warp-svc.log; exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
log "warp-svc is up"
|
||||
|
||||
# --- 3. registration (persisted in /var/lib/cloudflare-warp) ---------------
|
||||
if warp-cli --accept-tos registration show >/dev/null 2>&1; then
|
||||
log "existing WARP registration found, reusing it"
|
||||
else
|
||||
log "no registration found, creating a new one"
|
||||
warp-cli --accept-tos registration new
|
||||
fi
|
||||
|
||||
warp-cli --accept-tos mode "$WARP_MODE" >/dev/null
|
||||
log "mode set to $WARP_MODE"
|
||||
|
||||
# --- 4. connect and wait ----------------------------------------------------
|
||||
warp-cli --accept-tos connect >/dev/null
|
||||
for i in $(seq 1 "$WARP_CONNECT_TIMEOUT"); do
|
||||
if warp-cli --accept-tos status 2>/dev/null | grep -q "Connected"; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
STATUS="$(warp-cli --accept-tos status 2>/dev/null | tr -s '[:space:]' ' ')"
|
||||
log "warp-cli status: $STATUS"
|
||||
|
||||
# --- 5. prove it: Cloudflare must report warp=on and give us the egress IP --
|
||||
TRACE="$(curl -fsS --max-time 15 "$TRACE_URL" || true)"
|
||||
WARP_FLAG="$(echo "$TRACE" | sed -n 's/^warp=//p')"
|
||||
EGRESS_IP="$(echo "$TRACE" | sed -n 's/^ip=//p')"
|
||||
COLO="$(echo "$TRACE" | sed -n 's/^colo=//p')"
|
||||
|
||||
if [[ "$WARP_FLAG" == "on" || "$WARP_FLAG" == "plus" ]]; then
|
||||
log "WARP tunnel verified: ip=$EGRESS_IP warp=$WARP_FLAG colo=$COLO"
|
||||
else
|
||||
log "ERROR: Cloudflare does not see this container as a WARP client (warp='${WARP_FLAG:-none}', ip='${EGRESS_IP:-none}')"
|
||||
log "warp-svc log tail:"; tail -n 30 /var/log/warp-svc.log || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 6. run the bot ---------------------------------------------------------
|
||||
log "starting bot: $*"
|
||||
exec "$@"
|
||||
Reference in New Issue
Block a user