Deploy a Telegram or Discord bot
Run a Python bot in a cardinal container with persistent code, a resource budget, and auto-restart.
Bots are the classic "set and forget" workload: they need to stay up, keep
their state, and not eat the whole VPS. cardinal fits them well — a small
container, a mounted code directory, and --restart always means the bot
comes back on its own.
The pattern is the same for Telegram and Discord: put the bot code on the
host, mount it into the container, install dependencies on start, and run.
1. The pattern
mkdir -p /data/mybot
cd /data/mybot
cardinal run -d --restart always \
-n mybot \
-v /data/mybot:/bot \
--workdir /bot \
--memory 256m --cpus 0.25 --disk 1G \
-e BOT_TOKEN="your_token" \
--startup @/bot/start.sh \
python:3.11-slim
-v /data/mybot:/bot— edits on the host are instantly visible inside.--startup @/bot/start.sh— a script that installs dependencies and runs
the bot (it overrides the image CMD).
--memory 256m --cpus 0.25 --disk 1G— a hard resource budget so the bot
can never take the server down.
2. Telegram bot
mkdir -p /data/tg-bot && cd /data/tg-bot
bot.py:
import os
from telegram import Update
from telegram.ext import Application, CommandHandler
TOKEN = os.environ["BOT_TOKEN"]
async def start(update: Update, context):
await update.message.reply_text("Hello from cardinal Telegram bot!")
async def ping(update: Update, context):
await update.message.reply_text("pong")
def main():
app = Application.builder().token(TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("ping", ping))
app.run_polling()
if __name__ == "__main__":
main()
requirements.txt:
python-telegram-bot==20.7
start.sh:
#!/bin/sh
set -e
pip install --no-cache-dir --disable-pip-version-check -r /bot/requirements.txt
exec python /bot/bot.py
Then run it:
cardinal run -d --restart always \
-n tg-bot \
-v /data/tg-bot:/bot \
--workdir /bot \
--memory 256m --cpus 0.25 --disk 1G \
-e BOT_TOKEN="YOUR_TELEGRAM_BOT_TOKEN" \
--startup @/bot/start.sh \
python:3.11-slim
3. Discord bot
Same layout, discord.py instead:
mkdir -p /data/discord-bot && cd /data/discord-bot
bot.py:
import os, discord
from discord.ext import commands
TOKEN = os.environ["BOT_TOKEN"]
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
@bot.command()
async def ping(ctx):
await ctx.send("pong")
bot.run(TOKEN)
requirements.txt:
discord.py==2.4.0
Same start.sh, same run command with -n discord-bot and the Discord
token.
4. Bot with a database
For persistence, pair the bot with a PostgreSQL container and connect over
the cardinal0 bridge (the host gateway is 10.0.2.1):
cardinal run -d --restart always \
-n bot-db \
-v bot_pgdata:/var/lib/postgresql/data \
--memory 1g --cpus 1 --disk 10G \
-e POSTGRES_DB=botdb -e POSTGRES_USER=bot -e POSTGRES_PASSWORD=secret \
postgres:16
Then point the bot at it with -e DB_HOST=10.0.2.1 and connect using
asyncpg or psycopg2.
5. Updating the bot
nano /data/tg-bot/bot.py # edit code on the host
cardinal restart tg-bot # restart to apply
Or copy a file in without restarting:
cardinal cp ./bot.py tg-bot:/bot/bot.py
6. Health monitoring
cardinal logs -f tg-bot # follow stdout
cardinal ps | grep bot # is it running?
cardinal restart discord-bot
--restart always already brings the bot back after a crash or reboot —
the persistent cardinal-bootstrap supervisor handles it, with no daemon of
its own.