Back to Blog
Tutorial9 min read2026-06-30

How to Deploy a Discord Bot in Python

Deploy a discord.py bot to always-on hosting: the async event loop, app commands and syncing, privileged intents, why it's a worker process, and adding a database for state.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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=...) then sync(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_TOKEN as 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).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also