Python's [discord.py](https://discordpy.readthedocs.io) is the most popular library for building Discord bots in Python. Like its Node counterpart, a discord.py bot maintains a persistent gateway connection, which shapes how you host it. This guide gets a discord.py bot running reliably in production.
It's an async worker, not a web app
discord.py is built on asyncio. Your bot opens a WebSocket to Discord's gateway and runs an event loop forever, reacting to events. There's no HTTP port to expose to users — the bot dials out and stays connected. So you deploy it as a long-running background worker, and it must not scale to zero, or it disconnects when idle and misses events.
A minimal bot with app commands
Modern discord.py uses slash ("application") commands via app_commands:
import os
import discord
from discord import app_commands
intents = discord.Intents.default()
intents.message_content = True # privileged; enable in portal too
class Bot(discord.Client):
def __init__(self):
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
await self.tree.sync() # register slash commands
bot = Bot()
@bot.tree.command(name="ping", description="Check latency")
async def ping(interaction: discord.Interaction):
await interaction.response.send_message(f"Pong! {round(bot.latency*1000)}ms")
bot.run(os.environ["DISCORD_TOKEN"])Privileged intents
The message_content intent is privileged — you must enable it in the Discord Developer Portal under your application's Bot settings *and* set it in code. Forgetting the portal toggle is the single most common reason a Python bot connects but never reacts to message content. Members and presence intents are similarly gated.
Syncing commands carefully
tree.sync() registers your slash commands with Discord. Syncing globally on every startup is wasteful and subject to rate limits and up to ~1 hour propagation. Best practice:
- During development, sync to a specific test guild for instant updates:
await self.tree.copy_global_to(guild=...)thensync(guild=...). - In production, sync globally only when commands actually change, not on every boot.
Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "bot.py"]Pin discord.py in requirements.txt. The library can also auto-detect via buildpacks, but an explicit Dockerfile keeps the Python version under your control.
Deploying as an always-on worker
On PandaStack, deploy from your Git repo as a container service with no public port, and inject DISCORD_TOKEN as a secret. Because gateway bots must stay connected, run it on an always-on tier rather than a scale-to-zero free-tier app — a sleeping bot is an offline bot. discord.py handles reconnects automatically as long as the process stays alive, so the platform's job is simply to keep it running and restart it if it crashes.
Adding a database
Keep bot state out of memory. Attach a managed PostgreSQL and read DATABASE_URL (it's auto-wired). Use an async driver like asyncpg to match discord.py's event loop:
import asyncpg, os
pool = await asyncpg.create_pool(os.environ["DATABASE_URL"])For scheduled actions — daily reminders, cleanup — a cronjob that runs a small script is cleaner than a tasks.loop you have to keep alive, and it survives bot restarts.
Go-live checklist
- Deployed as an always-on worker (not scale-to-zero)
DISCORD_TOKENas a secret- Privileged intents enabled in portal + code
- Command sync strategy (guild in dev, global on change)
asyncpg+ managed Postgres for state- Cronjobs for scheduled actions
References
- [discord.py documentation](https://discordpy.readthedocs.io/en/stable/)
- [discord.py app commands](https://discordpy.readthedocs.io/en/stable/interactions/api.html)
- [Discord gateway intents](https://discord.com/developers/docs/topics/gateway#gateway-intents)
- [asyncpg](https://magicstack.github.io/asyncpg/current/)
A discord.py bot runs as an always-on PandaStack worker with auto-wired managed Postgres for state and cronjobs for schedules. Deploy yours on the free tier at [dashboard.pandastack.io](https://dashboard.pandastack.io).