To run a Discord bot 24/7 on a VPS, you package the bot in a Docker image, pass the token as an environment variable and set a restart policy so Docker brings it back after a crash or a reboot. The steps below cover the Dockerfile, the env file, the run command and how to read the logs when something goes wrong.
Before you start
You need:
- A Linux VPS with SSH access. Your IP, username and password are in your VPS provider's control panel.
- Docker installed. If it is not there yet, follow Install Docker and Docker Compose on a Linux VPS.
- A Discord application and bot user created in the Discord Developer Portal, with the bot token copied somewhere safe.
- Either your own bot code or the small example below.
- About 15 minutes.
Note: A bot that serves a few servers runs fine on a small VPS. A Python or Node bot usually idles well under 100 MB of RAM, so memory is rarely the limit — long log files are a more common problem.
Prepare the bot project
Create a working directory and move into it:
mkdir -p /opt/discord-bot
cd /opt/discord-botCreate requirements.txt with the library:
discord.pyThen create bot.py with a minimal bot that replies to a ping:
import os
import discord
TOKEN = os.environ["DISCORD_TOKEN"]
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
@client.event
async def on_ready():
print(f"Logged in as {client.user}", flush=True)
@client.event
async def on_message(message):
if message.author == client.user:
return
if message.content == "!ping":
await message.channel.send("pong")
client.run(TOKEN)The token is read from the environment with os.environ, never hard-coded.
Tip: Once the bot works, pin the library version in requirements.txt so a rebuild months later does not pull a breaking change. Check the library's own docs for the version you should pin.
Write the Dockerfile
Create /opt/discord-bot/Dockerfile:
FROM python:3-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py ./
RUN useradd --create-home --shell /usr/sbin/nologin bot
USER bot
CMD ["python", "bot.py"]Three things matter here. PYTHONUNBUFFERED=1 makes print output appear in docker logs immediately instead of sitting in a buffer. Copying requirements.txt before the code keeps the dependency layer cached, so code edits rebuild in seconds. And the USER bot line means the process does not run as root inside the container.
Build the image:
docker build -t my-discord-bot:1.0 .Expect a few lines of pip output and a final line naming the image my-discord-bot:1.0.
Pass the token as an environment variable
Warning: Anyone who has your bot token controls your bot. Never put it in the Dockerfile, in bot.py, in a Git repository or on the command line — command-line values end up in the shell history.
Create the env file and lock it down:
nano /opt/discord-bot/bot.env
chmod 600 /opt/discord-bot/bot.envPut one line inside it, with no quotes and no spaces around the =:
DISCORD_TOKEN=PASTE_YOUR_TOKEN_HEREDocker reads this file at start-up and injects DISCORD_TOKEN as an environment variable inside the container. The file stays on the host with mode 600, so only root can read it.
Run a Discord bot 24/7 on a VPS with Docker
Start the container:
docker run -d \
--name discord-bot \
--restart unless-stopped \
--env-file /opt/discord-bot/bot.env \
--log-opt max-size=10m \
--log-opt max-file=3 \
my-discord-bot:1.0What each flag does:
-druns the container in the background and prints its ID.--name discord-botgives it a stable name fordocker logs,docker restartanddocker stop.--restart unless-stoppedrestarts the container after a crash and after a VPS reboot, unless you stopped it yourself on purpose.--env-fileloads the token from the file you just created.--log-opt max-sizeand--log-opt max-filecap the log at 10 MB per file and keep three files, so a noisy bot cannot fill the disk.
No port mapping is needed. A Discord bot opens outbound connections, so there is nothing to expose or open in a firewall. If you want the full list of flags, see Docker run flags you will actually use.
Check the logs
Confirm the container is running:
docker ps --filter name=discord-botRead the last 50 lines:
docker logs --tail 50 discord-botFollow the log live until you press Ctrl+C:
docker logs -f discord-botA healthy start looks like Logged in as YourBot#1234, followed by the bot joining its servers.
Check it works
docker pslistsdiscord-botwith statusUp.docker logs discord-botshows the login line.- In a channel the bot can read, type
!pingand expectpong. - Test the restart policy: run
docker kill discord-bot, wait a few seconds, then rundocker psagain. The container should be back up on its own. - Test a reboot: run
sudo reboot, reconnect over SSH once the server is back and checkdocker psagain. The container starts automatically because of the restart policy.
Troubleshooting
- Log says the token is invalid and the container exits — the token was copied wrong or has been regenerated in the Developer Portal. Copy the current token into
bot.env, then recreate the container:docker rm -f discord-botand run thedocker runcommand again. - The bot appears online but never answers — the message content intent is not enabled for your application in the Developer Portal. Enable it there, or switch the bot to slash commands, which do not need that intent.
- The container restarts in a loop — a crash on start-up, usually a missing dependency or a syntax error.
docker logs --tail 100 discord-botshows the traceback. Add the missing package torequirements.txt, rebuild and recreate the container. - Edits to
bot.pyhave no effect — you changed the file but not the image. Rebuild with a new tag, for exampledocker build -t my-discord-bot:1.1., then recreate the container. See Roll back a bad deploy using Docker image tags. - The disk fills up — logs are not capped. Recreate the container with the
--log-opt max-size=10mand--log-opt max-file=3flags shown above.
