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 run handles both signals by default, closes the connection cleanly and lets the process exit. The native Effect run handles them only with process, because an Effect launcher such as Node 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 inpackage.json, because the SDK is ESM-only - A bot started with
run. The default API stops cleanly on SIGINT and SIGTERM unlessBot processisSignals false. The native Effect API does so only whenprocessisSignals true, and otherwise relies on its launcher to stop the program on those signals - The token read from
process, never written in a source file, committed to version control or copied into an image.env .FLUXER_ BOT_ TOKEN - No extra exit handling: When a failure stops the bot,
runsetsBot processto 1, so the process manager can tell a crash from a normal stop.exit Code
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
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 process is false, the SDK listens for SIGINT and SIGTERM and for no other signals. The native Effect run listens only with process, so enable it when nothing else handles the signals and a drained stop is wanted. Without it, a launcher such as Node 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
drainoption ofMs runchanges that time, andBot drainskips the waitMs: 0 - 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 completewith its duration, removes its signal listeners and resolves therunResultBot
The SDK never calls process. The process exits once nothing else keeps Node.js running. After run 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 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 drain, 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 run resolves. Increase the timeout when drain 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_ in the environment for readable lines instead
The native Effect API uses the application's Effect logger unless logging 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
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_ makes it wait longer after each quick failure. PM2 stops an app by sending SIGINT and force-kills it after kill_, 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/
[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_ in /etc/, 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 Timeout 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 Kill 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
.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_, systemd through Restart 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 run 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 Configuration before connecting.
The reliability guide and troubleshooting explain these failures