On This Page

Deploy a Bot

Keep a bot running with PM2, systemd or Docker, with the token in the environment and a clean shutdown

A deployed bot is an ordinary Node.js process. A process manager starts it, passes the token through the environment, restarts it after a crash and stops it with a signal: SIGINT under PM2 and SIGTERM under systemd and Docker. A bot started with the default API's runBot handles both signals by default, closes the connection cleanly and lets the process exit. The native Effect runBot handles them only with processSignals: true, because an Effect launcher such as NodeRuntime.runMain already interrupts the program on both signals

Prepare the Bot

Every setup below expects the same project:

  • Node.js 24.15 or newer on the server or in the image
  • The "type": "module" field in package.json, because the SDK is ESM-only
  • A bot started with runBot. The default API stops cleanly on SIGINT and SIGTERM unless processSignals is false. The native Effect API does so only when processSignals is true, and otherwise relies on its launcher to stop the program on those signals
  • The token read from process.env.FLUXER_BOT_TOKEN, never written in a source file, committed to version control or copied into an image
  • No extra exit handling: When a failure stops the bot, runBot sets process.exitCode to 1, so the process manager can tell a crash from a normal stop

For local runs, keep the token in a .env file that version control ignores, and let Node.js load it

node --env-file=.env bot.js

The file holds one KEY=value line per variable. Write the value without quotes so Docker's --env-file reads the same file correctly

.env
FLUXER_BOT_TOKEN=paste-the-token-here

For TypeScript, run bot.ts instead of bot.js. Node.js 24 runs TypeScript files directly, so no build step is required

What Happens on Shutdown

In the default API, unless processSignals is false, the SDK listens for SIGINT and SIGTERM and for no other signals. The native Effect runBot listens only with processSignals: true, so enable it when nothing else handles the signals and a drained stop is wanted. Without it, a launcher such as NodeRuntime.runMain interrupts the program instead, which skips the drain below, and with no launcher Node.js ends the process at once. When the process receives one of the signals, the SDK logs the signal and shuts the bot down:

  • Stops accepting new events and gives running handlers, commands and scheduled tasks, the events already waiting for them and their requests up to 5 seconds to finish. Scheduled tasks that have not started are cancelled. The drainMs option of runBot changes that time, and drainMs: 0 skips the wait
  • Signals the handlers and tasks still running to stop through their signal
  • Closes the command router and every event subscription
  • Closes each gateway connection, giving it up to 5 seconds to close normally
  • Saves resumable sessions when a session store is configured. Saves for local shards run in parallel, each with a 5-second timeout
  • Waits for the SDK's own cleanup of sockets, requests and collector callbacks
  • Finally logs Shutdown complete with its duration, removes its signal listeners and resolves the runBot Result

The SDK never calls process.exit. The process exits once nothing else keeps Node.js running. After runBot resolves, close database connections, timers and servers the application opened. A returned handler Promise, a running scheduled task and their REST requests get the drain window to finish. Track detached Promises, timers not started with client.schedule and work that needs more than that window, as shown in application supervision

Use a 30-second stop timeout before the process manager force-kills the bot. With the default drainMs, allow up to 5 seconds for the drain, up to 5 seconds to close gateway connections and up to 5 seconds for parallel session saves when a store is configured, plus time for remaining SDK cleanup and the application's cleanup after runBot resolves. Increase the timeout when drainMs is larger or application cleanup needs longer. A force-kill before session saves finish can prevent resuming after restart. Each listener handles only the first signal of its kind, so sending the same signal again during shutdown gets Node.js's default handling and usually ends the process at once

Logs

The default API writes Info and Debug records to standard output and warnings and errors to standard error. When output is not a terminal, as under every setup below, each record is one JSON object per line, ready for a log collector. Set FLUXERLY_LOG_FORMAT=pretty in the environment for readable lines instead

The native Effect API uses the application's Effect logger unless logging.format is set explicitly. Set logging: { format: "json" } for the same JSON output, or logging: { format: "pretty" } for readable lines. The logging guide covers levels and categories

Run with PM2

PM2 reads an ecosystem file. Because the project uses "type": "module", name it ecosystem.config.cjs

module.exports = {
    apps: [
        {
            name: "fluxer-bot",
            cwd: __dirname,
            script: "bot.js",
            node_args: "--env-file=.env",
            exp_backoff_restart_delay: 1000,
            kill_timeout: 30000,
        },
    ],
}

PM2 restarts the bot whenever it exits, and exp_backoff_restart_delay makes it wait longer after each quick failure. PM2 stops an app by sending SIGINT and force-kills it after kill_timeout, whose default of 1.6 seconds is shorter than a graceful gateway close

pm2 start ecosystem.config.cjs
pm2 logs fluxer-bot
pm2 save

The pm2 save command records the running apps, and pm2 startup prints the command that starts PM2 again after a reboot

Run with Systemd

Create a unit file such as /etc/systemd/system/fluxer-bot.service

[Unit]
Description=Fluxer bot
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=fluxer-bot
WorkingDirectory=/srv/fluxer-bot
EnvironmentFile=/etc/fluxer-bot.env
ExecStart=/usr/bin/node bot.js
Restart=on-failure
RestartSec=5
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target

Put FLUXER_BOT_TOKEN=... in /etc/fluxer-bot.env, owned by root and readable only by root (chmod 600). The service manager reads the file before starting the service, so the bot's own user does not need access to it. Adjust the node path to the output of command -v node

By default, systemd stops a service with SIGTERM and waits TimeoutStopSec before force-killing it. The Restart=on-failure setting restarts the bot after a non-zero exit code or a crash, but not after a clean stop

sudo systemctl daemon-reload
sudo systemctl enable --now fluxer-bot
journalctl -u fluxer-bot -f

For a supervisor that runs child processes, add KillMode=mixed so SIGTERM reaches only the parent, which then stops its children

Run with Docker

Keep the token and local files out of the image with a .dockerignore file next to the Dockerfile

.dockerignore
.env
node_modules
FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "bot.js"]

The node:24-slim image tracks the newest Node.js 24 release. Adjust the copy and install lines for pnpm or another lockfile

docker build -t fluxer-bot .
docker run -d --name fluxer-bot --init --stop-timeout 30 --restart unless-stopped --env-file .env fluxer-bot
docker logs -f fluxer-bot

The token reaches the container only at run time through --env-file. The --init flag runs a small init process that forwards signals to Node.js. The docker stop command sends SIGTERM, and --stop-timeout 30 on docker run gives shutdown 30 seconds before a force-kill. With --restart unless-stopped, Docker restarts the bot after it exits unless it was stopped on purpose

Restart Policy

Restart after a crash, but with a delay: A bot that fails at startup, for example with a rejected token, fails again at once. Each setup above adds one: PM2 through exp_backoff_restart_delay, systemd through RestartSec and Docker through its own growing restart delay. Each restart creates a new client and a new gateway session. Events that arrived while the bot was down are not replayed unless a session store lets a quick restart resume the previous session

Which Failures End the Process?

The SDK keeps the bot running through handler failures, dropped events and lost connections, and reconnects on its own. The runBot Result fails, and the process exits with code 1, for a startup failure such as a rejected token or an unreachable instance, a connection failure the SDK cannot recover from, or a failed setup callback. Invalid options throw ConfigurationError before connecting. The reliability guide and troubleshooting explain these failures