diff --git a/Dockerfile b/Dockerfile index c963d63..5f6886b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -121,9 +121,12 @@ EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD wget -qO- "http://127.0.0.1:${PORT:-3000}/api/health" >/dev/null 2>&1 || exit 1 +COPY --chmod=755 docker-entrypoint.sh /docker-entrypoint.sh + USER node # Next.js standalone server (node process). Plain `node server.js` — Bun is # installed for the `docker exec` one-shot bins, not for the web runtime # (Next is shipped and tested on Node). +ENTRYPOINT ["/bin/sh", "/docker-entrypoint.sh"] CMD ["node", "server.js"] \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..cc04dda --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# docker-entrypoint.sh — Polaris Task Force container entrypoint (PID 1). +# Always starts the web server (CMD: node server.js, received as "$@"). +# Starts the Discord bot (`bun run bot`) ONLY when DISCORD_TOKEN is set +# (Coolify per-environment activation gate; dev/stage leave it unset). +# Forwards SIGTERM/SIGINT to both children, reaps them, and exits with +# the web server's exit status. +"$@" & +WEB_PID=$! +BOT_PID="" +if [ -n "${DISCORD_TOKEN:-}" ]; then + echo "[entrypoint] DISCORD_TOKEN is set — starting Discord bot" >&2 + bun run bot & + BOT_PID=$! +else + echo "[entrypoint] DISCORD_TOKEN is not set — Discord bot disabled" >&2 +fi +forward() { + signal=$1 + [ -z "$WEB_PID" ] || kill -"$signal" "$WEB_PID" 2>/dev/null || true + [ -z "$BOT_PID" ] || kill -"$signal" "$BOT_PID" 2>/dev/null || true +} +trap 'forward TERM' TERM +trap 'forward INT' INT +while kill -0 "$WEB_PID" 2>/dev/null; do + wait "$WEB_PID" + status=$? +done +if [ -n "$BOT_PID" ]; then + kill -TERM "$BOT_PID" 2>/dev/null || true + wait "$BOT_PID" 2>/dev/null || true +fi +exit "${status:-0}" diff --git a/src/bot/index.ts b/src/bot/index.ts index 56dc010..aef7cf8 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -15,6 +15,29 @@ const main = async () => { intents: [GatewayIntentBits.Guilds], }); + let shuttingDown = false; + const shutdown = async (signal: NodeJS.Signals): Promise => { + if (shuttingDown) { + payload.logger.warn(`[Bot] ${signal} received again — forcing exit`); + process.exit(143); + } + shuttingDown = true; + payload.logger.info(`[Bot] ${signal} received — shutting down gracefully`); + try { + if (client.isReady()) { + await client.destroy(); + } + payload.logger.info("[Bot] Discord client destroyed"); + process.exit(0); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + payload.logger.error(`[Bot] Error during shutdown: ${message}`); + process.exit(1); + } + }; + process.on("SIGTERM", () => void shutdown("SIGTERM")); + process.on("SIGINT", () => void shutdown("SIGINT")); + const commandExecutors = new Map( commands.map((command) => [command.builder.name, command.execute]), );