Deploying a Python Bot — What the Build Agent Decides for You
When you upload a Python project as a zip, a build agent reads your source and decides four things: which Python version to run, which install command to use, what command starts your app, and whether you need a port. It also sets PYTHONUNBUFFERED=1, which is why your print() output actually shows up in the logs. Most of this is inferred correctly from files you already have — but the inference has specific rules, and knowing them explains both why it usually just works and what to fix when it doesn't.
Python version comes from your dependencies
The default is a current Python, but the agent deliberately prefers 3.11 when it sees signals that a stack often lacks 3.12 wheels — aiogram==2.x, older python-telegram-bot, torch builds, very old Pillow. Otherwise 3.12 is fine.
That's a more useful default than it sounds. The failure it avoids is a dependency with no prebuilt wheel for a newer Python trying to compile from source in the build and failing on a missing compiler or header. If your project pins old libraries, you get an interpreter they actually have wheels for.
If you care about the version, say so in pyproject.toml or a runtime marker rather than leaving it to inference.
Legacy bot libraries are handled
This one matters if you're following an older tutorial, which a lot of bot authors are.
- aiogram: if the code uses v2-only APIs —
executor, most obviously — the install is pinned toaiogram==2.25.1rather than pulling v3, which would fail immediately on the changed API. - python-telegram-bot: code using the old
FiltersAPI gets pinned to the 13.x line.
So a bot written against a 2021 tutorial deploys rather than exploding on an import. Worth knowing because the symptom if this went wrong would look like your code being broken, when it's actually a major-version mismatch.
You should still pin your own versions in requirements.txt — relying on inference is fine for getting running, not for reproducibility.
The install command follows your manifest
| What's in your project | What runs |
|---|---|
requirements.txt |
pip install -r requirements.txt |
pyproject.toml with Poetry |
pip install poetry && poetry install --no-root |
pyproject.toml, normal package |
pip install . from the manifest directory |
| No manifest, but third-party imports | A pip install built from the imports it found |
| No manifest, no third-party imports | Nothing to install |
Two things worth noticing.
Poetry is handled properly. poetry install --no-root is used rather than pip install ., which is the right call for a project that isn't a distributable package — pip install . on a Poetry app is a common source of confusing build failures elsewhere.
A missing requirements.txt isn't fatal. A single bot.py that imports discord still deploys; the install command is assembled from what your code actually imports. That's genuinely useful for a small script, but include a manifest anyway — inference gets you running, a manifest gets you the same versions twice.
The start command stays simple
The agent prefers the simplest thing that works: python bot.py, python main.py, python -m package.module. If you have a start.sh or run.sh that clearly prepares the app and is executable, that's used instead.
It won't introduce Gunicorn or Uvicorn workers. Startup is kept dead simple on purpose, which is the right default on a small container — multiple workers multiply memory rather than sharing it. If you want a production server with a specific worker count, put it in your own start command or write a Dockerfile.
The wrapper-folder trap
Here's the one that catches people zipping a GitHub download.
When you download a repo as a zip, GitHub wraps everything in a project-main/ folder. Your entry point ends up at project-main/src/bot.py. The agent handles this — the working directory becomes the wrapper folder and the run command is relative to it, so python src/bot.py — but it's worth understanding, because it explains why paths in your own code should be relative to the project root rather than absolute.
Cleaner still: zip the contents of your project directory, not the directory itself.
Ports: bots get none
If your code starts a web server, the port is exposed. If it doesn't — a Discord bot, a Telegram poller, a queue consumer — no port is mapped at all.
That's the entire web-versus-worker distinction, and it's why a bot needs no special configuration and no fake HTTP server to look busy.
PYTHONUNBUFFERED=1 and why you care
This gets set automatically, and it's the difference between logs that work and logs that appear to be missing.
By default Python buffers stdout when it isn't attached to a terminal. In a container that means your print() calls and any logging to stdout sit in a buffer instead of reaching the log stream — so a bot that crashes can produce no output at all, because the buffer died with the process.
With it set, output is unbuffered and shows up immediately. If you write your own Dockerfile, set it yourself:
ENV PYTHONUNBUFFERED=1
It's the first thing to check if you're deploying a Python app and the logs look emptier than they should.
Secrets
Hard-coded tokens in your source won't fail the build — the agent doesn't reject a project for having a key in a file. That's leniency about deploying, not an endorsement: move it to an environment variable, and rotate anything that's been committed.
If the agent can tell your app needs configuration it can't find, it pauses and asks for those values by name, and the platform sets them without the agent ever seeing them.
FAQ
Which Python version will my app get?
Inferred from your dependencies — 3.11 when your pins suggest a stack that lacks newer wheels, otherwise 3.12. Declare it explicitly in pyproject.toml if you care.
Does Poetry work?
Yes. A Poetry pyproject.toml gets poetry install --no-root, not pip install ., which is the correct treatment for an application rather than a package.
Do I need a requirements.txt?
Not strictly — imports are detected and installed. Include one anyway so you get the same versions on every build.
Will my old aiogram 2 bot deploy?
Yes. Code using v2-only APIs is pinned to aiogram==2.25.1 rather than being given v3, which would break on the changed API. The same applies to old python-telegram-bot code.
Why are my Python logs empty?
Python buffers stdout when not attached to a terminal. PYTHONUNBUFFERED=1 is set for you on this path; set it yourself in a hand-written Dockerfile.
Do I need Gunicorn or Uvicorn workers?
Not by default, and they won't be added for you. On a small container a single process is usually right — add a production server yourself only if you've measured that you need it.
Wrapping up
Inferred Python builds work because Python projects advertise themselves — a requirements.txt, a pyproject.toml, an import list, an obvious entry point. The rules reading those are specific rather than magical, and the two most useful to know are the ones that quietly save you: an interpreter chosen to match your dependencies' wheels, and unbuffered output so you can see what your bot is doing.