Deploying a Pywa Bot
What your project needs
Section titled “What your project needs”- Dependency metadata that declares
pywa[server]itself, pinned to a version. Supported files, in precedence order:uv.lock,pylock.toml,pyproject.toml, thenrequirements.txt. Pywa Cloud’s own tooling has no opinion on which pywa version you run — that’s yours to choose, the same way it would be for any other dependency. - A discoverable
WhatsAppinstance — found the same waypywa runalready finds one: a conventionally named file (main.py,app.py,wa.py, orbot.py, at the project root or underapp/,bot/,wa/, orsrc/) containing a module-level variable namedwa,bot,client,app, ormain.
That’s it — this is an ordinary pywa project, not a special format.
If your project doesn’t fit that convention — a different filename, a differently-named variable — set an entrypoint override instead of restructuring your project:
pywa cloud bots create my-bot --slug my-bot --entrypoint-override my_module:my_whatsapp_instance# or on an existing bot:pywa cloud bots set-entrypoint <bot-id> my_module:my_whatsapp_instance# and to go back to default discovery:pywa cloud bots set-entrypoint <bot-id>Only takes effect on the next deploy or restart, same as any other setting baked into the running container at launch time.
Python version
Section titled “Python version”Also controlled by your project, never hardcoded by the platform:
requires-pythoninpyproject.toml(e.g.">=3.12","==3.11.*") — the newest version Pywa Cloud supports that satisfies it is used.- A
.python-versionfile (thepyenv/uvconvention) — used directly if it’s a supported version. - Neither present → the newest version Pywa Cloud supports.
A pin that no supported version satisfies is a build-time error, not a silent fallback.
Monorepo support
Section titled “Monorepo support”By default, deploy treats the archive it uploads as the project itself — dependency file and
WhatsApp instance both resolved from the archive root. If your bot actually lives in a
subdirectory of a larger repository (a monorepo with other services alongside it), point the bot
at that subdirectory instead of restructuring the repo:
pywa cloud bots create my-bot --slug my-bot --bot-directory bots/backend# or on an existing bot:pywa cloud bots set-directory <bot-id> bots/backend# and to go back to deploying from the archive root:pywa cloud bots set-directory <bot-id>bots/backend must be a relative path with no .. segments — the deployable subdirectory needs
its own dependency file and its own discoverable WhatsApp instance (or its own entrypoint
override), exactly as if it were the whole project; there’s no support for a single dependency
lockfile shared across multiple monorepo members. Everything else in the archive (other services,
a root README, etc.) is uploaded but never inspected.
What deploy actually does
Section titled “What deploy actually does”pywa deploy [path] packages the directory (see Exclusions below)
into an archive and uploads it. The control plane then:
- Resolves your project’s Python version (above).
- Installs dependencies with
uvinto the bot’s container environment, usinguv.lock,pylock.toml,pyproject.toml, orrequirements.txtin that order. - Builds a container image — the same base runtime contract pywa’s own CLI is built on, not a separate reimplementation.
- Stops any container already running for this bot, and starts the new one — every deploy
replaces the previous container, it never runs alongside it. This isn’t a limitation to work
around; it’s deliberate. A pywa bot’s in-memory state (pending
wa.listen()calls, the duplicate-webhook cache) only makes sense in exactly one running process at a time. - Injects your bot’s environment variables and secrets into the container (see Environment Variables & Secrets).
If the build fails, pywa deploy prints the build log and exits non-zero — a failed deploy
is a good time to see everything the build step said, not just “it failed.”
What gets excluded
Section titled “What gets excluded”Two layers:
-
Always excluded, no matter what:
.venv,.git,__pycache__,.pywacloud, anything starting with.env. This isn’t configurable — you don’t want your git history or a virtualenv in an upload regardless of what your.gitignoresays. -
.gitignore, respected by default — the same files git already wouldn’t track generally won’t be uploaded either. -
.pywacloudignore— same syntax as.gitignore(one pattern per line,!to un-ignore), but evaluated after.gitignore, so it can override it in either direction: exclude something git tracks but a deploy doesn’t need (tests/,docs/), or include something git ignores but a deploy does need (a built frontend bundle indist/, say):.pywacloudignore tests/!dist/
link and unlink
Section titled “link and unlink”A directory’s first deploy writes .pywacloud/link.json (itself git-ignored) so every later
deploy in that directory knows which bot to target. Without --bot <bot-id>, and with a real
terminal attached, deploy prompts you to pick an existing bot or create a new one; pass --bot
directly (required in CI, where there’s no terminal to prompt) to skip the prompt. You can also
link explicitly ahead of time:
pywa cloud link <bot-id> [path]pywa cloud unlink [path]Deploying from CI
Section titled “Deploying from CI”--bot/a linked directory work fine in CI, but they still deploy with your personal login. For a
credential scoped to exactly one bot — and nothing else — see
deploy tokens:
export PYWA_CLOUD_TOKEN=<a deploy token>export PYWA_CLOUD_BOT_ID=<bot-id>pywa deployWhat’s still a work-in-progress simplification
Section titled “What’s still a work-in-progress simplification”Worth knowing if you’re deciding whether to rely on this yet:
- The control plane you run locally is also what builds your container (
docker build/docker rundirectly) — there’s no separate build service or object storage yet. Fine for local development; not the intended production design. Built images are tagged per-deployment and kept locally (enabling rollback — seedeployments rollbackin the CLI reference), but nothing prunes old tags. SetPYWA_CLOUD_IMAGE_REGISTRY(e.g.localhost:5000orghcr.io/your-org) on the control plane to also push every build there and have rollback fall back to pulling from it if the local tag is ever gone — off by default, so out of the box images still only live on whichever machine built them. - Redeploys are stop-old-then-start-new — a brief window of real downtime, not a zero-downtime
cutover. This is a deliberate correctness choice, not an oversight: running two containers
briefly for the same WhatsApp number risks a webhook being processed twice, or a
wa.listen()reply landing on the wrong process. - Runtime logs (
logs/logs --follow) anddocker stats-based metrics only cover what the container itself is currently holding onto or reporting live — nothing is retained once the container is gone.