Skip to content

Deploying a Pywa Bot

  • Dependency metadata that declares pywa[server] itself, pinned to a version. Supported files, in precedence order: uv.lock, pylock.toml, pyproject.toml, then requirements.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 WhatsApp instance — found the same way pywa run already finds one: a conventionally named file (main.py, app.py, wa.py, or bot.py, at the project root or under app/, bot/, wa/, or src/) containing a module-level variable named wa, bot, client, app, or main.

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:

Terminal window
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.

Also controlled by your project, never hardcoded by the platform:

  1. requires-python in pyproject.toml (e.g. ">=3.12", "==3.11.*") — the newest version Pywa Cloud supports that satisfies it is used.
  2. A .python-version file (the pyenv/uv convention) — used directly if it’s a supported version.
  3. Neither present → the newest version Pywa Cloud supports.

A pin that no supported version satisfies is a build-time error, not a silent fallback.

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:

Terminal window
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.

pywa deploy [path] packages the directory (see Exclusions below) into an archive and uploads it. The control plane then:

  1. Resolves your project’s Python version (above).
  2. Installs dependencies with uv into the bot’s container environment, using uv.lock, pylock.toml, pyproject.toml, or requirements.txt in that order.
  3. Builds a container image — the same base runtime contract pywa’s own CLI is built on, not a separate reimplementation.
  4. 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.
  5. 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.”

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 .gitignore says.

  • .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 in dist/, say):

    .pywacloudignore
    tests/
    !dist/

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:

Terminal window
pywa cloud link <bot-id> [path]
pywa cloud unlink [path]

--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:

Terminal window
export PYWA_CLOUD_TOKEN=<a deploy token>
export PYWA_CLOUD_BOT_ID=<bot-id>
pywa deploy

What’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 run directly) — 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 — see deployments rollback in the CLI reference), but nothing prunes old tags. Set PYWA_CLOUD_IMAGE_REGISTRY (e.g. localhost:5000 or ghcr.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) and docker stats-based metrics only cover what the container itself is currently holding onto or reporting live — nothing is retained once the container is gone.