Your bot runs from one .py file on a
read-only filesystem. That rules out a .env file — but not the
code that reads one. Two ways work:
Set DISCORD_TOKEN in the dashboard, keep it out of your source.
TOKEN = os.getenv("DISCORD_TOKEN")Works, but the token then lives in your file — we recommend environment variables instead.
TOKEN = "MTk4NjIyNDgzNDcx…"python-dotenv? Then change nothing at all in your
code — just add the variables in the dashboard. Why that works:
see below.
One NAME=value per line. Same format as a .env file, so you can paste yours straight in:
DISCORD_TOKEN=MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs
PREFIX=!
Plain os.getenv
import os
import discord
from discord.ext import commands
TOKEN = os.getenv("DISCORD_TOKEN")
PREFIX = os.getenv("PREFIX", "!") # second argument = default
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix=PREFIX, intents=intents)
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
bot.run(TOKEN)
You do not have to rewrite a script built around a .env
file. load_dotenv() finds no file here, does nothing, and raises nothing —
and os.getenv() then reads the variable we set instead. The same file runs
on your laptop with a .env and here without one.
import os
import discord
from dotenv import load_dotenv
load_dotenv() # no .env here — silently does nothing
TOKEN = os.getenv("DISCORD_TOKEN") # comes from the dashboard instead
client = discord.Client(intents=discord.Intents.default())
client.run(TOKEN)TOKEN = os.getenv("DISCORD_TOKEN")
if not TOKEN:
raise SystemExit("DISCORD_TOKEN is not set — add it with the key icon on the bot card.")TOKEN="abc" equals TOKEN=abc.
# lines are ignored, an export prefix is accepted.PATH,
PYTHONPATH, LD_PRELOAD, …).Works, but not recommended
This works, and plenty of hosted bots run this way. We still recommend against it. A token in your source file travels with every copy of that file — into a backup, a chat message, a version control system like Git. From a public repository a Discord token is harvested within minutes, and whoever has it fully controls your bot. Discord itself scans public repositories and invalidates tokens it finds — which protects you, but takes your bot offline without warning.
The safe way is the environment variables in the first tab: they never appear in your file, are never shown again in the dashboard, and do not show up in the server's process list.
import discord
TOKEN = "MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs"
client = discord.Client(intents=discord.Intents.default())
client.run(TOKEN)The token now lives in your source file. We never serve that file to the web, but you still have a copy of it — so do not paste it into a Discord channel, a pastebin, a screenshot or a public repository. If it does get out, regenerate it in the Discord Developer Portal and upload the corrected file.
| Environment variables | In the script | |
|---|---|---|
| Token visible in your file | No | Yes |
| Sharing the script is safe | Yes | No |
| Change the token | Key icon, no re-upload | Edit and re-upload the file |
| Setup effort | Paste one line in the dialog | None |
All of these are perfectly normal on your own machine. They fail here for one of two
reasons: only a single .py is uploaded, or the filesystem is read-only.
Click a row for the fix. The error messages are the real ones, taken from a container
running with the production settings.
The upload takes one .py file, so no .env ever reaches the server. load_dotenv() itself stays quiet — which is why this is so easy to miss — and the token simply ends up as None.
from dotenv import load_dotenv
load_dotenv() # no error, but no values either
TOKEN = os.getenv("DISCORD_TOKEN") # -> None
client.run(TOKEN)Fix: keep the code exactly as it is and set DISCORD_TOKEN in the dashboard.
from dotenv import dotenv_values
TOKEN = dotenv_values(".env")["DISCORD_TOKEN"] # KeyError: 'DISCORD_TOKEN'
with open(".env") as f: # FileNotFoundError
...The same goes for a config.json, config.yaml or token.txt next to your script: nothing but the one .py exists. Fix: os.getenv(), or put the value in the script.
from config import TOKEN # config.py was never uploaded
from cogs.music import Music # same problem, one folder deeperFix: merge your project into a single file. There is no way around this one — it is a limit of the platform, not of your code. (If the name is a package instead, it simply is not installed.)
TOKEN = os.environ["DISCORD_TOKEN"] # raises when unsetNot wrong as such — it only fails if the variable is missing. Either set it in the dashboard, or use os.getenv(...), which returns None instead of raising.
with open("token.txt", "w") as f: # also /tmp, also a SQLite file
f.write(token)The container may not write anywhere on disk — deliberately, because that is part of what keeps bots isolated from each other. Keep state in memory, or in a database you reach over the network.
TOKEN = input("Token: ")Nobody is sitting at a console: your bot is started by the platform, not by you in a shell.
An export DISCORD_TOKEN=… in your .bashrc, a systemd unit or your IDE's run configuration lives on your computer. Our container starts with the variables you set in the dashboard, and nothing else.
Your bot's own output is under View Output on its card.
| What the output says | What it means |
|---|---|
| TypeError: expected token to be a str, received NoneType instead | The variable is not set, or is spelled differently, so the token is
None. Check the name with the key icon. |
| LoginFailure: Improper token has been passed. | Discord rejected the token: empty, mistyped, or regenerated in the Developer Portal. We stop the bot instead of retrying — a rejected token never fixes itself. |
| KeyError: 'DISCORD_TOKEN' | os.environ[…] or dotenv_values(…)[…] on
something that is not set. |
| ModuleNotFoundError: No module named 'config' | A second file of your project — merge everything into one
.py. With a package name it means the package is not
installed. |
| OSError: [Errno 30] Read-only file system | Your script tries to write to disk. Not possible here. |
| What you do | Result |
|---|---|
os.getenv("DISCORD_TOKEN") + variable set here | Works — recommended |
load_dotenv() + os.getenv() + variable set here | Works — no code changes |
Token written directly in the .py | Works |
os.environ["DISCORD_TOKEN"] + variable set here | Works |
Uploading a .env, config.json or config.py | Not possible — one .py per bot |
load_dotenv() without setting variables here | Token stays empty |
| Reading or writing files next to the script | Fails — read-only filesystem |
input() for the token | Fails — no terminal |
In clear text, in a file only the web server can read, handed to your container when it starts. Clear text is unavoidable: your bot needs the real token to log in to Discord. Two consequences worth knowing:
Treat a bot token like a password: it grants full control of the bot account. If you think it leaked, regenerate it in the Discord Developer Portal and save the new one here.
Upload your .py, paste your variables, press Start.