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.