Deploying a Python Bot — What the Build Agent Decides for You

·5 min read·Updated
pythondiscordtelegrambotspoetryhow-to

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 to aiogram==2.25.1 rather than pulling v3, which would fail immediately on the changed API.
  • python-telegram-bot: code using the old Filters API 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.